Customs Check
Check for free

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: thirteen cross-field rules, four document completeness rules and the validators specific to each declaration system. It files nothing and queries no tariff database — it checks the internal consistency and the format of your file 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. 1Create a key from your account, under “API keys”.
  2. 2Send it in the Authorization header of every request.
  3. 3Revoke 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": 0 },
  "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": [],
  "engine": { "version": "1.0.0", "rules": 13 }
}

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": [] } ] }'

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 and for consistency with the operation, never against a tariff database: a well-formed code remains a code to verify.