If you ship physical goods to the US, every product needs a 10-digit HTS code on the customs entry. Those codes usually come from one of three places: the supplier's invoice, last year's spreadsheet, or more and more often an AI chat answer. All three have the same problem: nobody checks whether the code still exists.
It is easy to get wrong. Here are two real examples from this week's schedule (USITC HTS 2026 Revision 22):
-
8507.60.0020is the code CBP rulings from 2016 to 2025 gave lithium-ion power banks. It is not in the current schedule. The US later split lithium-ion batteries into.10(electric vehicles),.30(energy storage systems) and.90(everything else). -
6201.40.99.99looks perfectly plausible for a men's synthetic jacket. It never existed. Language models are very good at producing codes with the right shape.
A wrong code is not just a typo. It decides the duty rate, which permits apply and which extra duties (Section 301, 232) are added, and CBP can assess penalties for negligent misclassification.
How an HTS code is built
The first six digits are the international Harmonized System, the same in every country. The US adds four more:
6109.10.00.12
│││└┴── 4-digit heading (6109: T-shirts, singlets, tank tops, knitted)
││└──── 6-digit HS subheading (6109.10: of cotton) ← international
│└───── 8-digit US subheading (6109.10.00): sets the duty rate
└────── 10-digit statistical suffix (.12: men's) ← US only
A few things that trip up anyone who tries to validate codes with a simple lookup table:
-
Rates live on the 8-digit line. The 10-digit suffix only exists for statistics, so
6109.10.00.12has no rate of its own and inherits 16.5% from6109.10.00. -
Many 6-digit subheadings have no row. The schedule is a tree of indented descriptions, and
6201.40("Of man-made fibers:") is only a heading for the lines under it. It is a valid code to talk about, but you cannot declare it. -
Unnumbered levels matter. Between
6201.40and6201.40.70.00sit description-only rows such as "Other:" and "Water resistant". The legal meaning of a line is its full path, not its last description. -
The schedule changes often. USITC has published 22 revisions of the 2026 schedule so far. Suffixes get split, merged and renumbered, which is exactly what happened to
8507.60.0020.
Checking codes offline
I needed this check in a few places (a website, a browser extension, scripts), so I packaged the current schedule with a small pure-Python API. It bundles USITC's data (about 0.5 MB compressed, all 35,805 rows), has no dependencies and needs no API key:
pip install hts-lookup
import hts_lookup as hts
hts.TITLE # 'Revision 22 (2026)'
line = hts.lookup("6109.10.00.12")
line.description # "Men's (338)"
line.general # '16.5%' (inherited from the 8-digit line)
[c for c, _ in line.path] # ['6109', '6109.10.00', '', '', '6109.10.00.12']
hts.is_valid("6201.40.99.99") # False
hts.closest("6201.40.99.99").code # '6201.40'
hts.lookup("6201.40").declarable # False: a subheading with lines under it
hts.lookup("8471.30").code # '8471.30.01.00' (only one line under it)
hts.is_valid("8507.60.0020") # False: the old power bank suffix
[l.code for l in hts.children("8507.60.00")]
# ['8507.60.00.10', '8507.60.00.30', '8507.60.00.90']
closest() walks back from 10 to 8, 6 and 4 digits until it finds a code that exists, which is usually what you want to show a user next to "this code is not valid". children() gives the next level down, so you can narrow a heading to a declarable line.
There is also a CLI:
$ hts-lookup 6201.40.99.99
6201.40.99.99 is not in the US tariff schedule (Revision 22 (2026)).
Closest valid code: 6201.40 Of man-made fibers:
Letting AI assistants check instead of guess
Since a lot of these wrong codes come from chat answers, the package also runs as an MCP server, so Claude, ChatGPT and other MCP clients can look codes up in the official schedule instead of generating them:
{
"mcpServers": {
"hts-lookup": { "command": "uvx", "args": ["hts-lookup", "--mcp"] }
}
}
It exposes four tools: lookup_code, validate_code, search_tariff_text and list_children. The server is about 140 lines of standard library: newline-delimited JSON-RPC 2.0 on stdin and stdout, which is all the MCP stdio transport needs. It is listed in the official MCP Registry as net.hs-code/hts-lookup.
What a lookup cannot tell you
A valid code is not the same as the right code. 6109.10.00.12 is a perfectly valid line, and still wrong for a polyester T-shirt, which belongs in 6109.90 at twice the duty. Classification depends on facts about the goods (fiber content, knitted or woven, how it is used) that no lookup table knows.
For that part I write step-by-step guides that ask the questions customs actually uses and quote the CBP rulings behind each answer, at hs-code.net. The package handles the boring but important first step: making sure the code you are about to declare exists.
The data is public domain US government work. The general rate is the MFN rate; Chapter 99 additional duties depend on the country of origin and are not applied.
Top comments (0)