USITC HTS API Guide: Accessing US Tariff Data
The US International Trade Commission publishes the Harmonized Tariff Schedule via a free REST API. No authentication required. Here's how to use it, what it returns, and the gotchas that will waste your time if you don't know about them.
Base URL and endpoints
Base: https://hts.usitc.gov/reststop. No API key needed. No rate limit documented (but be polite — add delays between requests).
| Endpoint | What it does |
|---|---|
/search?keyword=X | Full-text search across descriptions |
/getRates?htsno=XX&keyword= | All codes in a chapter (01-97) |
/currentRelease | Current HTS revision identifier |
Fetching all codes in a chapter
# Get all codes in Chapter 85 (Electrical machinery) curl -s "https://hts.usitc.gov/reststop/getRates?htsno=85&keyword=" | python3 -c " import sys, json data = json.load(sys.stdin) print(f'{len(data)} codes in chapter 85') for item in data[:3]: print(f' {item[\"htsno\"]:16s} {item.get(\"description\",\"\")[:50]}')" # Output: # 1296 codes in chapter 85 # 8501 Electric motors and generators... # 8501.10 Of an output not exceeding 37.5 W # 8501.10.20.00 Synchronous motors of an output...
Each item returns: htsno, description, general (MFN duty), special (FTA/GSP rates), other (Column 2 rate), units, footnotes, and indent (hierarchy level).
Searching by keyword
# Search for "headphones" curl -s "https://hts.usitc.gov/reststop/search?keyword=headphones" | python3 -c " import sys, json for item in json.load(sys.stdin)[:5]: print(f'{item[\"htsno\"]:16s} {item.get(\"general\",\"\")} {item.get(\"description\",\"\")[:50]}')" # Output: # 8518.30 Headphones and earphones... # 8518.30.10.00 Line telephone handsets # 8518.30.20.00 Other
Gotcha #1: Chapter 99 isn't in getRates
Chapter 99 contains the Section 301/232 tariff provisions. You'd expect getRates?htsno=99 to return them. Instead of JSON, it returns the USITC website HTML — the endpoint silently fails for Chapter 99.
# This returns HTML, not JSON: curl -s "https://hts.usitc.gov/reststop/getRates?htsno=99&keyword=" # <!DOCTYPE html><html>... (the USITC website, not data) # Use search instead: curl -s "https://hts.usitc.gov/reststop/search?keyword=9903.88" | python3 -c " import sys, json data = [i for i in json.load(sys.stdin) if i['htsno'].startswith('9903.88')] print(f'{len(data)} provisions found')" # 70 provisions found
To get ALL Chapter 99 provisions, you need to search for each prefix 9903.01 through 9903.99. There are 712 total provisions covering Section 301, Section 232, reciprocal tariffs, and temporary duty suspensions.
Gotcha #2: Section 301 data is hiding in footnotes
This is the non-obvious part. To find which HTS codes are subject to Section 301 tariffs, you need to:
- Fetch all codes via
getRates(chapter by chapter) - Parse the
footnotesarray on each code - Look for references to
9903.XX.XXprovisions - Look up those provisions via
searchto get the rate and country
# Example: 8518.30.10.00 (headphones) has a Section 301 footnote curl -s "https://hts.usitc.gov/reststop/getRates?htsno=85&keyword=" | python3 -c " import sys, json, re for item in json.load(sys.stdin): if item.get('htsno') == '8518.30.10.00': for fn in item.get('footnotes', []): refs = re.findall(r'9903\.\d+\.\d+', fn.get('value', '')) if refs: print(f'Code: {item[\"htsno\"]}') print(f'Footnote: {fn[\"value\"]}') print(f'Chapter 99 refs: {refs}')" # Output: # Code: 8518.30.10.00 # Footnote: See 9903.88.15. # Chapter 99 refs: ['9903.88.15']
We parsed every footnote across all 26,118 HTS codes. Result: 10,562 codes have at least one Chapter 99 reference. Nobody else publishes this as structured data.
Gotcha #3: HTML in data fields
Some fields contain raw HTML that you need to strip:
# Description field can contain HTML: # "Digital signal processing apparatus <span class=\"article-footnote\" # title=\"See 9903.88.69. \">1/</span>" # The footnote reference is in the HTML title attribute! # The "1/" marker is the footnote indicator. # Strip HTML tags and footnote markers before displaying. import re desc = re.sub(r'<[^>]+>', '', raw_desc) # strip tags desc = re.sub(r'\s*\d+/', '', desc) # strip footnote markers
footnotes array. If you only parse the footnotes array, you'll miss these. Parse both the footnotes AND the HTML in general/description fields.
Gotcha #4: Broken hierarchy (orphan parent codes)
HTS codes have a parent-child hierarchy: 8518.30.20.00 → parent 8518.30.20 → parent 8518.30. But intermediate rows often don't exist in the data.
# 8518.30.20.00 has parent_code "8518.30.20" # But 8518.30.20 DOES NOT EXIST as a row in the USITC data # The hierarchy jumps from 10-digit to 6-digit # If you're walking up the hierarchy (e.g., to find duty rates # or footnotes on parent rows), you need to handle missing # intermediate levels by stripping dotted segments.
We found 9,366 orphan parent codes across the full HTS schedule. Any code that does hierarchy walks (for duty inheritance, description resolution, or footnote lookup) needs to handle this.
Gotcha #5: Duty rate format is not standardized
The general field (MFN rate) is a text string, not a number. Formats vary:
| Example value | What it means |
|---|---|
Free | 0% duty |
16.5% | Percentage of value (ad valorem) |
5¢/kg on contents and container | Specific duty (per unit weight) |
6.3¢/liter | Specific duty (per unit volume) |
Free 1/ | Free with footnote (the "1/" is a footnote marker) |
Free 1/ 1/ | Free with two footnotes (strip both) |
The duty provided in the applicable subheading + 25% | Chapter 99 additional rate |
You need to parse all of these. Regex ([\d.]+)% catches ad valorem rates. Specific duties (¢/kg, ¢/liter) need separate handling. Footnote markers (\d+/) need stripping.
Gotcha #6: No bulk download endpoint
There's no single-call way to get the entire schedule. You must iterate chapters 01-97 via getRates, making 97 sequential requests. At ~15 seconds per chapter, expect ~25 minutes for a full scan.
Data refresh: how to detect changes
# Check current revision curl -s "https://hts.usitc.gov/reststop/currentRelease" # Returns: "2026HTSRev4" # New revisions published ~monthly # Poll this endpoint and re-ingest when it changes
Or just use our API
We've already done all of this:
- 26,118 HTS codes with cleaned descriptions (HTML stripped, footnote markers removed, textile codes stripped)
- 10,562 codes with parsed Chapter 99 footnotes → Section 301/232 rates
- 712 Chapter 99 provisions with parsed rates, country, and authority
- 9,366 orphan parent codes fixed
- Full hierarchy walks for duty inheritance and description resolution
- Automatic refresh on HTS revision changes
# One call — get HTS code + duty + Section 301 + FTA + rulings curl -X POST https://htsapi.dev/v1/classify \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_KEY" \ -d '{"description": "bluetooth headphones", "country_of_origin": "China"}' # Or look up a specific code curl "https://htsapi.dev/v1/hts/8518.30.10.00" -H "X-API-Key: YOUR_KEY" # Or search curl "https://htsapi.dev/v1/hts/search?q=headphones" -H "X-API-Key: YOUR_KEY"