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).

EndpointWhat it does
/search?keyword=XFull-text search across descriptions
/getRates?htsno=XX&keyword=All codes in a chapter (01-97)
/currentReleaseCurrent 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
Search searches descriptions only, not footnotes. If you're looking for codes affected by Section 301, you can't search for "9903" and find affected codes. The footnotes aren't indexed by the search endpoint. See the Section 301 gotcha below.

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:

  1. Fetch all codes via getRates (chapter by chapter)
  2. Parse the footnotes array on each code
  3. Look for references to 9903.XX.XX provisions
  4. Look up those provisions via search to 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
Some 9903 references are ONLY in the HTML title attributes — not in the 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 valueWhat it means
Free0% duty
16.5%Percentage of value (ad valorem)
5¢/kg on contents and containerSpecific duty (per unit weight)
6.3¢/literSpecific 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:

# 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"
We built this API because the USITC API is hard to use correctly. If you're building a trade compliance tool and need clean HTS data with Section 301/232 duties, CBP rulings, and FTA rates pre-joined — see the developer guide or get an API key.

Sources

Related data source guides