HS code API: the EU tariff code from your own software
The engine of the free HS code finder, for your own system. Send a product description and get the suggested 6-digit HS code and 8-digit EU CN code, the official text of the tariff behind it, a short reason, a confidence level and what the description leaves out. One product a request, or up to 50.
A key is made for each company. Write to danil@freightparse.com with your company's name and the number of products you expect a month. The key is shown once: keep it on your server, never in a web page or an app.
One product
POST /api/v1/classify with a JSON body:
| Field | What it is |
|---|---|
description | What the product is, what it is made of, what it is for: 2 to 500 characters, English or Dutch. Required. |
material | What it is made of, at most 200 characters. Optional. |
use | What or who it is for, at most 200 characters. Optional. |
lang | en or nl: the language of the error messages. The official tariff text and the reasons are English. |
curl https://freightparse.com/api/v1/classify \
-H "Authorization: Bearer $FP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "T-shirt for men, knitted, short sleeves", "material": "cotton"}'The answer, shortened:
{"status": "ok", "code6": "610910", "code8": "61091000", "confidence": "high",
"why": "...", "vague": false, "better_description": null,
"line6": {"code": "610910", "text": "Of cotton", "undivided": false,
"path": [{"kind": "section", "code": "XI", "text": "Textiles and textile articles"}, ...]},
"line8": {"code": "61091000", "text": "Of cotton", "above": []},
"alternatives": [{"code6": "610990", "text": "Of other textile materials", "heading": "...", "why": "..."}],
"ask": false, "questions": [], "refine": null, "refined": null,
"units": 1,
"limits": {"per_minute": 60, "per_day": 1000, "per_month": 10000, "left_today": 999, "left_this_month": 9999},
"request_id": "9f0c2b6e4a1d4c3e8b7a6f5e4d3c2b1a"}| Field | What it holds |
|---|---|
status | ok (a code), none (no line of the tariff fits) or no_candidates (no word of the description is in the tariff). |
code6, code8 | The 6-digit HS code and the 8-digit CN code under it, or null. |
line6, line8 | The tariff's own words for the code, and its path from the section down to the line. |
confidence, why | The engine's own rating of how clearly the description points to one code, and a short reason. The rating is not a measured accuracy. |
alternatives | Up to three other codes that came close, with their tariff text. |
vague, better_description | Whether the description leaves out what decides the code, and what to add. |
ask, questions, refine | The questions, when the code depends on something the description does not say. |
units, limits | What this request counted, and what is left of the key's limits. |
When the description leaves out what decides the code
Then ask is true, code6 is the best guess and not final, and questions holds one or two questions. Their options are the tariff's own lines, each with the codes it allows, and Not sure last. To answer, send the same description again with the answer's refine and, for each question answered, the chosen option's codes. The server keeps nothing between the two requests, so the second one carries the description again.
curl https://freightparse.com/api/v1/classify \
-H "Authorization: Bearer $FP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "leggings",
"refine": {"compared": ["610463", "610462", "610461", "610469"], "uncertain": true},
"answers": [{"question": 1, "codes": ["610462"]}]}'When your answer leaves one line of the tariff, the code is taken from it. An answer the tariff settles by itself counts no unit; one that needs the language model again counts one.
Many products in one request
POST /api/v1/classify/batch with up to 50 products in items, each with your own id if you like. Identical products (the same words, whatever the capitals, accents and spaces) are classified once: the copies carry duplicate_of and count no unit.
curl https://freightparse.com/api/v1/classify/batch \
-H "Authorization: Bearer $FP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"items": [{"id": "SKU-1001", "description": "Wireless earbuds with a charging case"},
{"id": "SKU-1002", "description": "wireless earbuds with a charging case"},
{"id": "SKU-1003", "description": "Leggings", "material": "cotton"}]}'Every item comes back in its place with a status: the answer for ok, none and no_candidates; rejected for an item that is not valid (the rest still run); error when no answer could be had; not_run when the batch's 240 seconds ran out before it. Errors and items not run count nothing: send them again. Give your HTTP client 250 seconds for a batch.
Your usage
GET /api/v1/usage gives the key's units today and this month, and its limits.
curl https://freightparse.com/api/v1/usage -H "Authorization: Bearer $FP_API_KEY"Units, limits and prices
A unit is one product the engine answered after asking the language model. These count nothing: a request that is refused, a product no answer could be had for, a product a batch had no time for, a copy in a batch, and an answer to a question that the tariff settles by itself. The same product in two requests is two units, because nothing is kept between requests.
Each key has its own limits a minute, a day and a month, 60, 1,000 and 10,000 products unless agreed otherwise. A request over one gets 429 with Retry-After: the seconds until it can pass. A day starts at midnight UTC, a month on the first.
| Products a month | Price |
|---|---|
| The first 500 products a month | free |
| From the 501st to the 1,000th | €0.25 a product |
| From the 1,001st on | €0.15 a product |
Prices exclude VAT, counted per key and calendar month (UTC).
Errors
Every error is JSON with error, a message and the request_id, which every answer also has in its X-Request-Id header. Quote it when you write to us.
| Status | error | When |
|---|---|---|
| 400 | bad_json | The body is not a JSON object. |
| 401 | unauthorized | No key, or a key that is not valid. |
| 403 | suspended | The key is suspended. |
| 413 | too_large, too_many_items | The body is too large, or the batch has too many products. |
| 422 | too_short, too_long, bad_choice and others | A field is missing or not valid, or the answers do not match the questions. |
| 429 | rate_limit, day_limit, month_limit | Over a limit of the key: see Retry-After. |
| 502, 503, 504 | unavailable, busy, timeout | No answer could be had just now. Nothing was counted: try again. |
Where a description goes
Your description goes to our server and from there to a cloud language model, which proposes the code. Send no personal data: the engine does not need any.
Read in Google's EU region, not used for training. A request flagged by Google's abuse checks may be kept up to 90 days, in the EU. FreightParse keeps no copy on its server.
The server's log records for each request the key's id, how many products and units, the time and the model's tokens, never the words. For the bill it keeps the time, the key's id, the units and the request id of each request.
Check the code before you file it
Every code is a suggestion, not a ruling: a binding one, called a BTI, comes only from a customs authority. Compare the official text with the product, and look the code up in the EU's TARIC database. You are responsible for the code on your declaration.