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.
| 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.
- 1Create a key from your account, under “API keys”.
- 2Send it in the Authorization header of every request.
- 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_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": 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.
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 and for consistency with the operation, never against a tariff database: a well-formed code remains a code to verify.