Integrate Codici HS code lookups into your systems using our REST API.
Include your API key in every request via the api-key header:
Or as a query parameter: ?api_key=YOUR_API_KEY
Manage API keys in your account.
Look up an HS code for a single product description.
| Parameter | Required | Description |
|---|---|---|
| description | required | Product description in any language |
| region | optional | Customs region (default: global). Options: global, usa, european_union, great_britain, canada, china, japan, australia, saudi_arabia, faroe_islands |
| language | optional | Response language (default: en). Options: en, nl, fr, et, fo |
Classify up to 100 product descriptions in a single call. The response is an array of result objects in the same order as the descriptions you sent.
Billing counts lookups, not API calls. Each description in the batch is one lookup against your monthly allowance, so a call containing 100 descriptions uses 100 lookups — batching saves round trips and time, not cost.
Describe a tariff code you already have: its wording, its place in the nomenclature and any control-list flags. Use it when a person keys in or picks a code themselves and you want it shown the way a classified one is. Nothing is classified, so these requests do not count against your plan.
| Parameter | Required | Description |
|---|---|---|
| code | required | At least four digits, with or without punctuation |
| region | optional | Customs region (default: global) |
| language | optional | Language of the wording (default: en) |
| confidence | optional | How sure the choice of code was, 0 to 1 (so 0.9 means 90%). Flags carry this number; without it they carry 1. |
The response is not signed. known is true when the code is a line of the
region's nomenclature; otherwise description is null and no flags are checked. It also
carries classification_breakdown, restriction_flags, restriction_check,
notices and nomenclature, in the same shapes as a lookup result, and quota.
| Field | Meaning |
|---|---|
| quota.earned_24h | Descriptions earned by the lookups of the last 24 hours: the number of lookups times the number earned per lookup. |
| quota.used_24h | Code requests made in that window, including this one. |
| quota.remaining_24h | What is left: earned minus used. |
| quota.per_minute | The per-minute request limit. |
quota is only present when the request succeeds.
Access is earned by classifying. A key can use this endpoint only if it has classified a
description in the last 24 hours, and every successful lookup in that window earns a number of descriptions
(20 at the moment). A per-minute limit applies on top (30 at the moment). Every accepted request is counted, whatever its parameters, so requests that fail validation do not retry for free. quota shows what is left.
Without a recent lookup you get 403 lookup_required; past the earned number, 429 code_quota.
Where you stand this month: your plan, how many lookups it includes, how many you have used, how many remain, and any extras accrued beyond the allowance.
Your recent API usage records: when each lookup happened, from which key and which IP. This endpoint returns metadata only. The lookups themselves are recorded, and become visible in the portal once a company administrator switches on Keep requests.
Every lookup comes back in a signed envelope. The data object holds the request
echo, an anonymous requestor reference, the classification result and our platform details.
hmac is an HMAC-SHA256 signature over data, so a saved result can be
proven to have come from us, unaltered — paste it into the
verification page to check.
result_not_used sits deliberately outside data and is
therefore not covered by the signature. It carries fields we have not yet curated into
result and will shrink over time, so treat it as transitional.
Field shapes do not change with the region you request.
confidence and each alternative's likelihood are fractions between
0 and 1 (so 0.9 means 90% confident).
hs_code is the six-digit WCO Harmonized System code, written xxxx.xx
everywhere. hts_code is the full national code, written the way that authority
writes it:
| Region | Code | Notation | Example |
|---|---|---|---|
| European Union | Combined Nomenclature (8) / TARIC (10) | digits only | 88040000 |
| Great Britain | UK commodity code (up to 10) | digits only | 8804000000 |
| United States | HTSUS (10) | xxxx.xx.xx.xx | 8211.92.90.30 |
| Other regions | national schedule | dotted pairs | 3901.20.00.99.999 |
Compare codes on their digits, not their punctuation — strip everything that isn't a digit and the values are directly comparable across regions.
You do not need to keep our exact JSON. To reproduce the signature: take the data
object, sort every key alphabetically at every level, trim leading and trailing whitespace from
every string value, serialise it compactly, and compute
HMAC-SHA256 over that with your shared secret. Key order, indentation and stray
whitespace therefore make no difference — only the field values do. result_not_used
sits outside data and is deliberately not signed.
cost is what this one lookup added on top of your fixed monthly fee: 0
while you are inside your allowance, your plan's per-extra-lookup price once past it.
/v1/lookup-batch returns an array of these objects, one per description.
Every result records the edition of the tariff nomenclature its code and descriptions were taken from. It is
inside result, so it is covered by the signature: if you are asked months later which version a
classification was based on, the answer is in the result you kept, and the
verification page uses it to tell you whether the code has changed since.
| Field | Meaning |
|---|---|
| nomenclature.source | codici when the code and its levels came from our own copy of the schedule, classification_provider when they came from our upstream classifier (regions we do not hold yet), none when no code was returned, unavailable if the lookup could not be made. |
| nomenclature.as_at | The date the nomenclature was read for: the day the result was issued. |
| nomenclature.edition | The edition in force on that date. type is annual (a numbered edition, such as CN 2026) or rolling (a schedule that changes during the year, such as the UK and US ones). data_version identifies the loaded version of a rolling edition. |
| nomenclature.extension | A rolling edition that adds detail to an annual one: for the European Union, TARIC on top of the Combined Nomenclature. null when there is none. |
| nomenclature.shared_layer | The international (WCO) edition used to fill levels a national schedule does not publish. |
For the European Union, Great Britain and the United States, hts_code is the code as you would
enter it on a customs declaration: a complete 10-digit code (written with dots for the US, without separators
for the EU and Great Britain). The declaration block says whether the classified code is
complete and, when it is not, what to choose from. Other regions are not checked and
declaration.status is unchecked.
A classified code is not always one you can declare: the tariff may divide it further (a CN code split into
several TARIC lines), and we never pick a line for you, because that depends on the goods. The check is made
against the tariff as it stood on declaration.as_at, the day of the lookup, and is part of the
signed result.
| Field | Meaning |
|---|---|
| hts_code | When declaration.status is declarable, the complete 10-digit code to enter on the declaration. Otherwise the code as classified, which you should not enter as it stands. |
| declaration.status | declarable: nothing more specific sits under the code. needs_subdivision: the code is a real branch of the tariff but is divided further; choose from options. not_found: not a line of the tariff on that date. unchecked: this region is not checked, or the check could not be made. |
| declaration.code | The complete 10-digit declaration code, or null when the status is not declarable. |
| declaration.form | How the tariff holds the code: taric10 (a 10-digit TARIC line), cn8 (an 8-digit CN code with nothing under it, declared as the 8 digits plus 00) or stored (kept at another length, such as a 6-digit UK line). null when not declarable. |
| declaration.classified_as | The digits of the code as classified, before any check. |
| declaration.as_at | The date the tariff was checked on. |
| declaration.options | When the status is needs_subdivision: the lines directly beneath the code, each with its code, description (in the requested language where we have it) and whether it is itself declarable or divides again. Empty otherwise. |
| declaration.options_truncated | true when more lines exist than are listed. |
| alternatives | Each alternative is matched to a line of the tariff. A code that is a real line is shown in its complete 10-digit form when it can be declared; one that is not a line is cut back to its HS subheading (six digits), as a branch to consider, never as a code to declare. |
For the European Union we currently check these lists: dual-use goods (Regulation (EU) 2021/821, Annex I),
drug precursors (Regulations (EC) 111/2005 and 273/2004), the export rules for hazardous chemicals
(Regulation (EU) 649/2012, Annexes I and V), waste shipments (Regulation (EC) 1013/2006 and its replacement
2024/1157), import of cultural goods (Regulation (EU) 2019/880), CITES (endangered species) and ozone-depleting
substances and F-gases. The last two, and waste shipments, rest on coarse or dated correlation tables and say so
in each flag. Other regions are not checked yet, and
restriction_check.lists_checked says so.
Every result says whether the classified code may fall under a control list, so you can go back to your supplier for the datasheet or a written declaration before you export. A flag is a prompt to check, never a determination. And the absence of a flag is not clearance: the lists we check are indicative, a controlled item can be declared under an unexpected code, and whether an export is controlled often depends on the destination, end-user and end-use, which a lookup does not know about.
The structure is the same for every list, so lists we add later appear in the same fields.
These fields sit inside data.result, so they are part of the signature. Fields are only
ever added, never removed or renamed; platform.version changes when one is added.
| Field | Meaning |
|---|---|
| restriction_flags | One object per control list the classified code may fall under. An empty array means no flag, not that the item is clear. Check restriction_check to see which lists were looked at. |
| restriction_check.status | completed, or unavailable if the check could not run for this lookup. When it is unavailable the empty restriction_flags means nothing at all. |
| restriction_check.lists_checked | The lists this result was checked against, with the edition of the code mapping used. An empty array means we check no list for the region you asked about. |
| notices | Standing statements about what the check does not cover. Always show them with the flags. |
| list, list_name | Identifier and name of the control list the flag comes from. |
| status | Always possible_match: the code is linked to entries on the list, so check. It is never a statement that the item is controlled. |
| confidence | The classification's own confidence. A flag follows directly from the code, so it is exactly as reliable as the classification and carries the same number. It does not say how likely the item is to be controlled. |
| specificity | How much a match tells you: narrow (the code links to 1 entry and the list does not mark it "ex"), limited (2 to 4 entries, or 1 entry on a code marked "ex") or broad (5 or more). The more entries share one code, and the more goods under the code the list leaves out, the less a match says about your item. |
| match | exact: the code itself is listed. broader_code: a broader listed code includes yours. narrower_codes: your classification stops higher up and codes beneath it are listed, so a more specific classification would narrow this down. |
| partial_coverage | true when the list itself marks every matching code "ex": it applies to only some of the goods under the code, so most goods with this code are likely not affected. false means the list makes no such statement, not that the code is covered in full. |
| matched_codes | The listed codes that matched. |
| reference | The legal text the list comes from. |
| linked_entries, main_entries | How many distinct list entries the code links to, counting sub-entries, and how many main entries those fall under. |
| candidates | The main entries to check first: entry, its label as worded in the list (null if the entry is newer than the list edition we hold), the sub_entries that apply, and the source_refs that carry the link in the official tariff. These are leads, not findings. |
| candidates_truncated | true when the code links to more main entries than are listed here. |
| source_version | Which edition of the list, of the code mapping and of the tariff nomenclature this flag was produced from. Lists are revised at least yearly. |
| note | What a match on this list means and what to do about it. |
| scope_notice | What the check on this list does not cover, as one sentence. It is the same wording that appears in notices for this list, so an interface can show it next to the flag instead of every list's notice. null when the list has none. |
| HTTP status | Code | Meaning |
|---|---|---|
| 400 | missing_param / invalid_param / invalid_json / too_many | Missing or invalid parameters (too_many = more than 100 descriptions in one batch) |
| 401 | missing_key / invalid_key / key_revoked | Missing, unknown or revoked API key |
| 402 | no_plan | No plan is active on the account, so lookups are unavailable — contact us to get set up |
| 403 | lookup_required | /v1/code only: this key has not classified anything in the last 24 hours |
| 429 | rate_limit / code_quota | Rate limit exceeded, or (for /v1/code) all code descriptions earned in the last 24 hours are used |
| 500 | upstream_error | Internal error (upstream provider or system) |
Going over your included lookups is never an error — extra lookups are simply charged at your plan's per-lookup rate and appear on that month's invoice.