EU and China customs classification
Connect your system, start an EU or China classification, and retrieve the code, sources, and applied tariff version.
This API classifies a product in the requested jurisdiction:
| Jurisdiction | Result | Route |
|---|---|---|
| European Union | 8-digit CN and 10-digit TARIC | /v1/classify |
| China | 10-digit GACC commodity code and 3-digit CIQ suffix | /v1/classify-china |
Processing is asynchronous: create a job, poll its status, then read result when it is complete.
Official 2026 references
| Jurisdiction | Official reference | Application |
|---|---|---|
| European Union | Commission Implementing Regulation (EU) 2025/1926, CN 2026 | From 1 January 2026 |
| China | 2026 tariff adjustment plan, Announcement No. 11 of 2025 | From 1 January 2026 |
| China | General Administration of Customs tariff query | Operational code and rate verification |
The response is the technical record for each case: always read provenance.applied_tariff_date, provenance.tariff_date_basis and provenance.corpus_versions. latest_available_snapshot means that the latest available snapshot was applied during a tariff rollover window.
Get started in 3 calls
1. Configure the key
Open your organization from the Organizations area, create a key in Developer, then assign the required scope:
| Route | Required scope |
|---|---|
/v1/classify/... | classify |
/v1/classify-china/... | classify_china |
Keep the key on your server. Never expose it in a browser or mobile application.
export TTH_API_KEY="th_live_your_api_key"2. Create a job
curl -sS -X POST "https://api.thetradehub.eu/v1/classify/jobs" \
-H "X-API-Key: $TTH_API_KEY" \
-H "Idempotency-Key: product-4821-eu-20260910" \
-H "Content-Type: application/json" \
-d '{
"content": "Wireless Bluetooth headphones with active noise cancellation and USB-C charging",
"analysis_mode": "operational",
"locale": "en",
"classification_date": "2026-09-10"
}'A new request returns 202 Accepted. Replaying the same request with the same Idempotency-Key returns the existing job with 200 OK.
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "pending",
"analysis_mode": "operational",
"created_at": "2026-09-10T10:30:00Z",
"updated_at": "2026-09-10T10:30:00Z"
}3. Retrieve the result
Use the same route family that created the job.
curl -sS "https://api.thetradehub.eu/v1/classify/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-H "X-API-Key: $TTH_API_KEY"Poll after 1, 2, 4, then every 5 seconds until the status is completed or failed.
Complete ready-to-use example
This Python client works for both jurisdictions and checks HTTP errors.
import os
import time
import uuid
import httpx
BASE_URL = "https://api.thetradehub.eu"
API_KEY = os.environ["TTH_API_KEY"]
ROUTES = {"eu": "classify", "china": "classify-china"}
def classify_product(
content: str,
jurisdiction: str = "eu",
analysis_mode: str = "operational",
classification_date: str | None = None,
) -> dict:
route = ROUTES[jurisdiction]
timeout_seconds = 12 * 60 if analysis_mode == "operational" else 18 * 60
headers = {
"X-API-Key": API_KEY,
"Idempotency-Key": str(uuid.uuid4()),
}
payload = {
"content": content,
"analysis_mode": analysis_mode,
"locale": "en",
}
if classification_date:
payload["classification_date"] = classification_date
with httpx.Client(base_url=BASE_URL, headers=headers, timeout=30) as client:
response = client.post(f"/v1/{route}/jobs", json=payload)
response.raise_for_status()
job_id = response.json()["id"]
deadline = time.monotonic() + timeout_seconds
delay = 1
while time.monotonic() < deadline:
response = client.get(f"/v1/{route}/jobs/{job_id}")
response.raise_for_status()
job = response.json()
if job["status"] == "completed":
return job["result"]
if job["status"] == "failed":
raise RuntimeError(job.get("error", "Classification failed"))
time.sleep(delay)
delay = min(delay * 2, 5)
raise TimeoutError(f"Job {job_id} is still running")
result = classify_product(
"Stainless steel centrifugal pump for food liquids",
jurisdiction="china",
classification_date="2026-09-10",
)
decision = result["classification"]
china = result.get("china") or decision.get("china")
print(china.get("code13") or china["code10"])
print(decision["provenance"]["applied_tariff_date"])Where to find each value
| Data | EU | China |
|---|---|---|
| Recommended code | result.classification.recommended_code | result.china.code13 or result.china.code10 |
| Common HS code | First 6 digits of the code | result.china.hs6 |
| Confidence | result.classification.candidates[0].confidence | result.china.confidence |
| Legal reasoning | result.classification.candidates[0].legal_position | result.china.justification |
| Decision status | result.classification.status | same path |
| Applied tariff date | result.classification.provenance.applied_tariff_date | same path |
| Corpus versions | result.classification.provenance.corpus_versions | same path |
| Dossier identifier | result.session_id | result.session_id |
If result.classification.status is review_required, the engine returned a usable classification but requires human validation before declaration use.
Condensed EU result
{
"status": "completed",
"result": {
"session_id": "run_abc123",
"classification": {
"jurisdiction": "eu",
"status": "completed",
"recommended_code": "8518300090",
"candidates": [
{
"rank": 1,
"code": "8518300090",
"description": "Headphones and earphones",
"confidence": 0.94,
"legal_position": "Classification under the applicable GIRs and legal notes"
}
],
"provenance": {
"methodology_version": "sh-workflow-v1",
"applied_tariff_date": "2026-09-10T00:00:00Z",
"tariff_date_basis": "request",
"corpus_versions": {}
}
}
}
}Condensed China result
{
"status": "completed",
"result": {
"session_id": "run_def456",
"classification": {
"jurisdiction": "china",
"status": "completed",
"recommended_code": "8413709990999",
"provenance": {
"applied_tariff_date": "2026-09-10T00:00:00Z",
"tariff_date_basis": "request",
"corpus_versions": {}
}
},
"china": {
"code10": "8413709990",
"code13": "8413709990999",
"hs6": "841370",
"description_zh": "离心泵",
"confidence": 0.91,
"justification": "Classification under the Chinese tariff and national notes",
"ciq_candidates": [],
"selected_ciq": "999",
"declaration_elements": [],
"supervision_codes": [],
"quarantine_categories": [],
"units": "",
"mfn_rate": "",
"vat_rate": "",
"validated": true,
"verification_status": "operational",
"warnings": []
}
}
}The values above only illustrate the response structure. They are not classification advice for the described products.
Accepted request fields
| Field | Type | Required | Use |
|---|---|---|---|
content | string | Conditional | Detailed description, product sheet, or structured data |
image_urls | string[] | Conditional | Up to 5 HTTPS image URLs |
image_base64 | string | Conditional | Base64-encoded image |
image_media_type | string | With image_base64 | Image MIME type |
analysis_mode | operational or legal_deep | No | Defaults to operational |
locale | fr, en, es, or de | No | Response language, defaults to fr |
classification_date | ISO date YYYY-MM-DD | No | Tariff date to apply |
product_identity | object | No | Manufacturer, name, model, and reference |
technical_facts | object[] | No | Sourced technical characteristics |
transaction_context | object | No | Destination, end user, and known end use |
taric_code | string | China only | Optional comparison hint, never an automatic conversion |
At least one of content, image_urls, or image_base64 is required.
Statuses and timeouts
| Level | Values |
|---|---|
| Job | pending, processing, completed, failed |
| Decision | completed, review_required |
| Batch item | completed, failed, timeout |
The gateway allows up to 11 minutes for operational and 17 minutes for legal_deep. Use a slightly longer client deadline, such as 12 and 18 minutes. A 202 response means the job was accepted, not completed.
Batch classification
Use /v1/classify/batch for the EU or /v1/classify-china/batch for China.
A request contains 1 to 1,000 rows and returns one parent batch identifier. Processing is split into durable sub-batches of 100, with at most 32 active classifications. A 500-row file therefore produces 5 internal sub-batches, while a 1,000-row file produces 10. The service, not the client, manages this split.
curl -sS -X POST "https://api.thetradehub.eu/v1/classify-china/batch" \
-H "X-API-Key: $TTH_API_KEY" \
-H "Idempotency-Key: september-import-2026" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"id": "SKU-001", "content": "Radial ball bearing, 25 mm bore"},
{"id": "SKU-002", "content": "Stainless steel centrifugal pump"}
],
"locale": "en",
"analysis_mode": "operational"
}'Then retrieve the batch with GET /v1/classify-china/batch/{batch_id}. The items field contains results when the batch is complete. Keep a stable id for every row. After a partial failure, create a new batch containing only failed rows and use a new batch Idempotency-Key.
History, export, and sources
Replace {route} with classify or classify-china.
| Need | Request |
|---|---|
| List history | GET /v1/{route}/history?page=1&per_page=20 |
| Read one classification | GET /v1/{route}/history/{log_id} |
| Export CSV | GET /v1/{route}/history/export?from=2026-09-01&to=2026-09-30 |
| List frozen sources | GET /v1/{route}/runs/{run_id}/sources |
| Read one source | GET /v1/{route}/runs/{run_id}/source?source_id={source_id} |
Errors and retries
JSON errors always use this shape:
{
"error": "explicit error description"
}| HTTP | Recommended action |
|---|---|
400 | Fix the request, do not retry it unchanged |
401 | Check X-API-Key |
403 | Add the required scope or enable the module |
404 | Check the identifier and owning organization |
429 | Honor Retry-After, then retry with the same Idempotency-Key |
502, 503, 504 | Retry with backoff and the same Idempotency-Key |
Never log the API key, base64 images, or sensitive commercial data.
Last updated on