Developers
Customs check API
The same check as on this site, called from your own software. You send a file as JSON and get back a score out of 100, the blocking points, the warnings and the recommended actions — with stable anomaly codes and messages translated into the 24 official EU languages.
This is a server-to-server API. An API key is a secret. Calls from a browser are not supported and no CORS headers are sent on authenticated routes: a key that reaches a browser is a leaked key.
Overview
The API exposes the validator's check engine: fourteen cross-field rules, five TARIC rules, four document completeness rules and the validators specific to each declaration system. It files nothing: it checks the format and consistency of your file, whether the codes exist in the nomenclature and, for an import into the EU, the TARIC measures that apply to the origin (anti-dumping, controls, preferences, duty rate), before you prepare the declaration.
Every route is version-prefixed. The discovery routes and the specification are public: an integrator, or an agent, can read the schema before holding a key.
| Endpoint | Purpose | Auth |
|---|---|---|
| POST/v1/checks | Check one file and return the full report | Key required |
| POST/v1/checks/batch | Check up to 25 files in a single call | Key required |
| GET/v1/me | Return the current key and its quota, without consuming it | Key required |
| GET/v1/declaration-types | List the declaration types this API knows | Public |
| GET/v1/declaration-types/{type} | Describe the fields expected for one type | Public |
| GET/v1/openapi.json | The OpenAPI 3.1 specification of this API | Public |
Authentication
The API authenticates with a bearer token. The token is shown once, at creation: only a hash is stored, so a lost key is replaced, never recovered.
You can hold three active keys at a time, which lets you rotate a key without interrupting your integration: create the new one, deploy it, then revoke the old one.
- 01Create a key from your account, under “API keys”.
- 02Send it in the Authorization header of every request.
- 03Revoke it if it leaks: the cut is immediate, and the content of the files submitted with that key is purged right away.
Authorization: Bearer cc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxYour first check
A file is made of a type, a header and goods lines. Every value is a string — the engine takes care of interpreting them. Field names are discovered at runtime, type by type.
The response comes back immediately; there is nothing to poll afterwards.
curl https://www.customs-check.com/api/v1/checks \
-H "Authorization: Bearer $CUSTOM_CHECK_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "import-export",
"reference": "PO-2026-114",
"header": {
"flow": "IM",
"shipmentCountry": "CN",
"destinationCountry": "FR",
"incoterm": "FOB",
"companyName": "Acme SAS",
"companyCountry": "FR",
"eori": "FR12345678900012",
"transportMode": "sea",
"currency": "USD",
"invoiceTotal": "12500"
},
"items": [
{
"description": "Leather sport shoes",
"hsCode": "6403990000",
"quantity": "500",
"value": "12500",
"origin": "VN",
"grossMass": "850",
"netMass": "800"
}
],
"documents": { "commercialInvoice": "available", "packingList": "missing" }
}'Reading the response
Every anomaly carries both a machine identity and a readable sentence. Build your logic on the machine identity: it does not change without a version change.
- severity · code · path · rule
- Stable contractStable. New codes may appear; existing ones are not renamed without a version change.
- field · message · text
- Display onlyTranslated for display. They may be reworded at any time — never compare them in your code.
- engine.version
- Changes whenever a rule change moves the score of an unchanged file. Archive it alongside your reports.
The “path” field mirrors the shape of your request, so you can map an anomaly straight onto the matching field in your own interface.
header.<field> · items.<n>.<field> · documents.<category> · global
{
"id": "4c749e25-3408-48f9-b3ca-b18a7ad84681",
"createdAt": "2026-09-16T12:46:13.186Z",
"type": "import-export",
"locale": "en",
"reference": "PO-2026-114",
"score": 90,
"level": "good",
"counts": { "errors": 0, "warnings": 2, "infos": 1 },
"documents": { "commercialInvoice": "available", "packingList": "missing" },
"issues": [
{
"severity": "warning",
"code": "docPackingListMissing",
"path": "documents.packingList",
"rule": "DOC-002",
"params": null,
"field": "Packing list",
"message": "Packing list not provided",
"text": "Packing list not provided"
},
{
"severity": "warning",
"code": "docTransportMissing",
"path": "documents.transportDocument",
"rule": "DOC-003",
"params": null,
"field": "Transport document (B/L, AWB, CMR)",
"message": "Transport document not provided",
"text": "Transport document not provided"
}
],
"actions": [
{ "severity": "warning", "code": "docPackingListMissing",
"path": "documents.packingList", "text": "Add: Packing list" }
],
"tips": [
{
"severity": "info",
"code": "taricControlsPossible",
"path": "items.0.hsCode",
"rule": "RULE-017",
"params": { "measures": "Import control - CITES ; Import control on seal products" },
"field": "Product 1 — HS / commodity code (if known)",
"message": "Possible import controls depending on the goods (Import control - CITES ; Import control on seal products): check whether they apply.",
"text": "Product 1 — HS / commodity code (if known): Possible import controls depending on the goods (Import control - CITES ; Import control on seal products): check whether they apply."
}
],
"engine": { "version": "1.6.0", "rules": 25, "taric": "2026-10-01" }
}Discovering the fields
Declaration types and their fields are public, no key needed. This is the route to build a form, to feed a mapping from your ERP, or to give an agent the schema it has to fill.
The names returned here are exactly the keys to use in the header and in each goods line. Option lists come back resolved and translated; add the parameter to omit them if the response feels too heavy.
# The 8 declaration types
curl "https://www.customs-check.com/api/v1/declaration-types?locale=en"
# The fields of one type
curl "https://www.customs-check.com/api/v1/declaration-types/h7?locale=en"
# Same, without the resolved option lists
curl "https://www.customs-check.com/api/v1/declaration-types/h7?options=false"Batches
Up to 25 files per call. The response is always a success once the batch itself is accepted, and each entry carries its own status: one malformed file never fails the others.
If the remaining quota does not cover every valid entry, the whole batch is rejected rather than partially processed — you never have to guess where processing stopped.
curl https://www.customs-check.com/api/v1/checks/batch \
-H "Authorization: Bearer $CUSTOM_CHECK_KEY" \
-H "Content-Type: application/json" \
-d '{ "checks": [ { "type": "h7", "header": {}, "items": [] } ] }'Agents and tools
Besides the REST routes, the same engine and the same data are reachable from an AI assistant, through a Model Context Protocol (MCP) server, and from an API client, through a ready-made Postman collection. Both use the API key and the daily quota described above.
MCP server
The MCP server lets an assistant look up commodity codes, read the EU import duty of a code, check an EORI, VAT or AEO number against the official registers, and check a draft declaration, without you writing a line of glue code. It answers at POST /api/mcp, with JSON responses.
It is stateless and speaks both generations of the protocol: the 2026-07-28 revision, where each request carries its version, and the earlier revisions that open with an initialize call. Connection, discovery and listing the tools cost nothing; each tool call uses one unit of the key's daily quota.
Authentication is the same bearer token as the REST API. A client that only supports OAuth sign-in, such as the web connectors of claude.ai, cannot connect yet: use a client that lets you set a header.
POST https://www.customs-check.com/api/mcp
claude mcp add --transport http customs-check https://www.customs-check.com/api/mcp \
--header "Authorization: Bearer $CUSTOM_CHECK_KEY"| check_declaration | Check a draft declaration: score out of 100, level and every issue with the field and the action. Does not keep the file content unless asked. |
| list_declaration_types | The supported declaration systems, with authority, region and expected documents. |
| describe_declaration_type | The header and item fields of a type, to build the input of check_declaration. |
| search_hs_codes | Candidate Combined Nomenclature codes from a product description, in any of the 24 EU languages. |
| describe_hs_code | Whether a 4 to 10 digit code exists in the nomenclature in force, with its official path and wording. |
| lookup_import_duty | EU import duty of a code from the Commission's TARIC data: third-country duty, preferences for an origin, quotas, anti-dumping and controls. |
| search_chemical | A chemical substance by CAS number, CUS number or name in the Commission's ECICS inventory: the Combined Nomenclature code given for each entry, to verify. |
| get_quota_balance | Remaining balance of an EU tariff quota in the Commission's QUOTA database, from its order number or from a code and an origin: balance, state (open, critical, blocked, exhausted), period and the time of the reading. |
| validate_eori | Format of an EORI number and its status in the official register (European Commission, HMRC). |
| validate_vat | Format of an EU VAT number and its validity in VIES, with the registered name where published. |
| check_rex | Format of a registered exporter (REX) number and its status in the Commission's REX database (valid, revoked or unknown), with its dates. |
| screen_sanctions | A company, person or vessel against the EU, UN, UK and US sanctions lists, updated every day: each match with its score, strong or partial. |
| search_aeo | Search the Commission's database of Authorised Economic Operators by company name. |
- Commodity codes are suggestions taken from the wording of the nomenclature. Only the declarant, or a binding tariff information decision from customs, can confirm a classification.
- Duties are indicative: conditions, country exclusions and exhausted quotas are not evaluated. Every answer carries the reference date of the TARIC extract, refreshed monthly.
- A register that does not answer is reported as unavailable, never as an invalid number. Nothing is ever sent to customs.
Postman collection
The collection is generated from the OpenAPI specification, so it always matches it: one request per route, grouped by tag, with a body skeleton for each POST. Import it in Postman, then fill the apiKey variable (kept as a secret) and the requests are ready to send.
Download the collectionQuota
Each key gets 500 checks per UTC calendar day. The current state travels with every authenticated response, so you never need an extra call to read it.
Refused calls count too: a client looping on invalid requests throttles itself. The quota is set per key — write to us if you need more.
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 437
X-RateLimit-Reset: 1789603200
X-RateLimit-Policy: 500;w=86400Errors
Error messages are English only: they are protocol messages, meant for developers. Only the business content is translated.
Every response carries a request id, repeated in the body. Quote it when you contact us — it takes us straight to the call.
{
"error": {
"code": "invalid_request",
"message": "The request body failed validation.",
"details": [
{ "path": "items.0.quantity", "message": "expected a string" }
],
"requestId": "2bbb0946-6178-4b54-921c-b28f794c44bd"
}
}| Status | code | Meaning |
|---|---|---|
| 400 | invalid_json | The body is not valid JSON. |
| 422 | invalid_request | The JSON is valid but its content is not. The details name the offending field. |
| 401 | missing_credentials | Missing or malformed Authorization header. |
| 401 | invalid_key | Unknown key. |
| 401 | key_revoked | Revoked key. |
| 403 | account_suspended | The account owning the key is suspended. |
| 415 | unsupported_media_type | The content type is not JSON. |
| 413 | payload_too_large | Request body too large. |
| 429 | rate_limit_exceeded | Daily quota reached. See the Retry-After header. |
| 500 | internal_error | An error on our side. Retry, then report it with the request id. |
Data and retention
Files sent through the API are your customers' data: we keep as little as possible, and you have a switch to keep none at all.
- Send “store” as false and no file content is written at all: only the score and the anomaly codes are kept.
- Otherwise, file content is purged after 30 days.
- Revoking a key immediately purges the content of the files it submitted.
- Processing happens in Frankfurt and the database is hosted in the European Union.
- No file is uploaded through the API: you only declare which documents you hold.
200 items · 256 KB · 25 / batch · reference ≤ 64
What this API is not
The check helps you prepare. It is not a validation by the customs authorities, it files nothing, and it does not replace the regulatory obligations that apply to your operation.
Customs codes are checked for format, for existence in the nomenclature and, for imports into the EU, against the TARIC measures for their origin, never for the classification of the goods: an existing code remains a code to verify. TARIC measures are indicative (monthly extraction by the European Commission): country exclusions, exhausted quotas and conditional duties are not assessed.