EU customs tariff (TARIC) API client for Python: classify products into HS / CN / TARIC codes and get import duties.
A small client for the hscode.info HS code API. It covers HS code classification, HS code lookup and import duty for the EU Combined Nomenclature.
- German, English and French descriptions. Queries in any language: Zolltarifnummer, nomenclature combinée, tarif douanier.
- Zero dependencies. It uses the standard library only. Python 3.9 or newer.
- Typed. Responses are
TypedDicts and the package shipspy.typed. - Built for data teams: classify a pandas column or a product export in chunks of 25.
Note
Advisory only. Only a BTI from customs is legally binding.
pip install hscodeGet a key on your hscode.info account. The free plan has 30 AI units and 100 lookups a day, with no credit card. Paid plans start at $19 a month. See pricing.
from hscode import Client
client = Client(api_key="...") # or set HSCODE_KEY
result = client.classify("cotton t-shirt", mode="accurate")
top = result["results"][0]
print(top["goods_code"], top["description"])The CLI ships with the npm package, not with this one: npx @hscode/sdk classify "cotton t-shirt". See the TypeScript SDK and CLI.
Client(api_key=None, base_url="https://hscode.info", timeout=60, opener=None)
| Argument | Default |
|---|---|
api_key |
the HSCODE_KEY environment variable |
base_url |
https://hscode.info |
timeout |
60 seconds |
opener |
urllib.request.urlopen. Pass your own callable (request, timeout=...) to fake the network in tests. |
lang is "de", "en" or "fr" and defaults to "en". mode is "fast" (Quick, 1 AI unit) or "accurate" (Precise, 5 units, adds confidence and reasoning). When you omit year, the client looks up the latest snapshot once and reuses it.
Goods codes may contain spaces or dots. The client strips them and checks for 10 digits before it sends a request.
res = client.classify("frozen shrimp, peeled", mode="accurate", lang="de")
print(res["results"][0]["goods_code"], res["question"], res["disclaimer"])POST /api/v1/classify. Each result has goods_code, description, path, score, confidence, reasoning and duties.
res = client.classify_batch(["cotton t-shirt", {"id": "SKU-2", "text": "steel nails"}])
for item in res["items"]:
print(item["id"], item["results"][0]["goods_code"] if item["results"] else item["error"])POST /api/v1/classify/batch. One to 25 items per call. An item is a string or a dict with text and an optional id. A string gets its list position as id. An item that fails has error set.
detail = client.lookup("6109 10 00 10", year="2025", lang="fr")GET /api/v1/{lang}/{year}/codes/{code}. Returns the description, hierarchy, duties and signals such as quotas and restrictions. Pass history=True to add the duties of every yearly snapshot.
detail = client.history("6109100010")
for snapshot in detail["history"]:
print(snapshot["year"], len(snapshot["duties"]))Same as lookup with history=True. Use it for duty history by year.
hits = client.search("t-shirt")["items"]GET /api/v1/{lang}/{year}/search. Matches codes by words.
duties = client.duties("6109100010")["items"]GET /api/v1/{year}/duties/{code}. Import duty per origin, with legal base and measure type. Use it as the input of a customs duty calculator.
chapters = client.chapters()["items"]
rows = client.chapter("61")["items"]The nomenclature chapters, and the rows of one 2-digit chapter.
snapshots = client.snapshots()
print(snapshots["latest"])The yearly TARIC snapshots from 2016 to 2026.
classify_batch takes 25 items at most. Split the column in chunks of 25.
import pandas as pd
from hscode import Client
client = Client()
df = pd.read_csv("products.csv")
texts = df["description"].astype(str).tolist()
codes = []
for start in range(0, len(texts), 25):
chunk = texts[start : start + 25]
for item in client.classify_batch(chunk)["items"]:
top = item["results"][0] if item["results"] else None
codes.append(top["goods_code"] if top else None)
df["hs_code"] = codes
df.to_csv("classified.csv", index=False)pandas is only used in this example. The package does not depend on it.
Every failure raises HscodeError.
from hscode import Client, HscodeError
client = Client()
try:
client.lookup("1234")
except HscodeError as e:
print(e.status, e.code, e.message)| Field | Meaning |
|---|---|
status |
HTTP status (400, 401, 404, 429, 503). 0 for errors raised before a response. |
code |
The API error string, or invalid_request, missing_api_key and network_error for local errors. |
message |
A readable message. str(e) returns it. |
On HTTP 429 your plan quota is used up. See pricing.
On 1,000 real EU Binding Tariff Information (BTI, also called EBTI) rulings sampled from a pool of about 120,000, covering 94 chapters, the first suggestion is right at 2 digits (chapter) in 91 %, at 4 digits (heading) in 87.9 % and at 6 digits (subheading) in 84.7 %. As of October 2026, Precise mode. Details: accuracy.
Advisory only. Only a BTI from customs is legally binding.
The same tools are available as an MCP server at hscode.info/mcp: classify, lookup_code, search_codes and duty_history. Setup is on the developers page. See also llms.txt.
- hscode.info: HS code lookup and classification
- Developers
- API reference and OpenAPI spec
- Pricing
- HS codes by product
- TypeScript SDK and CLI on npm
- Source on GitHub
MIT license.