Skip to content
Customs Check
Sign upCreate an account

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.

EndpointPurposeAuth
POST/v1/checksCheck one file and return the full reportKey required
POST/v1/checks/batchCheck up to 25 files in a single callKey required
GET/v1/meReturn the current key and its quota, without consuming itKey required
GET/v1/declaration-typesList the declaration types this API knowsPublic
GET/v1/declaration-types/{type}Describe the fields expected for one typePublic
GET/v1/openapi.jsonThe OpenAPI 3.1 specification of this APIPublic

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.

  1. 01Create a key from your account, under “API keys”.
  2. 02Send it in the Authorization header of every request.
  3. 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Your 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.

Request
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

Response
{
  "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_declarationCheck 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_typesThe supported declaration systems, with authority, region and expected documents.
describe_declaration_typeThe header and item fields of a type, to build the input of check_declaration.
search_hs_codesCandidate Combined Nomenclature codes from a product description, in any of the 24 EU languages.
describe_hs_codeWhether a 4 to 10 digit code exists in the nomenclature in force, with its official path and wording.
lookup_import_dutyEU import duty of a code from the Commission's TARIC data: third-country duty, preferences for an origin, quotas, anti-dumping and controls.
search_chemicalA 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_balanceRemaining 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_eoriFormat of an EORI number and its status in the official register (European Commission, HMRC).
validate_vatFormat of an EU VAT number and its validity in VIES, with the registered name where published.
check_rexFormat of a registered exporter (REX) number and its status in the Commission's REX database (valid, revoked or unknown), with its dates.
screen_sanctionsA 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_aeoSearch 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 collection

Quota

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.

Response
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 437
X-RateLimit-Reset: 1789603200
X-RateLimit-Policy: 500;w=86400

Errors

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"
  }
}
StatuscodeMeaning
400invalid_jsonThe body is not valid JSON.
422invalid_requestThe JSON is valid but its content is not. The details name the offending field.
401missing_credentialsMissing or malformed Authorization header.
401invalid_keyUnknown key.
401key_revokedRevoked key.
403account_suspendedThe account owning the key is suspended.
415unsupported_media_typeThe content type is not JSON.
413payload_too_largeRequest body too large.
429rate_limit_exceededDaily quota reached. See the Retry-After header.
500internal_errorAn 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.