If you're building anything that touches cross-border trade — a landed cost calculator, a customs compliance tool, a Shopify app that shows import duty rates — you're going to need programmatic access to tariff data. This guide walks through every option, including the free government APIs, their limitations, and what to do when those limitations matter.
The data sources
There are two datasets you need for US/Canada cross-border work:
- US Harmonized Tariff Schedule (HTS) — maintained by the US International Trade Commission (USITC). 32,295 records covering every importable product category.
- Canadian Customs Tariff — maintained by the Canada Border Services Agency (CBSA). 22,461 records including MFN rates and preferential rates under CUSMA (the successor to NAFTA).
Both are public. Neither is particularly developer-friendly out of the box.
Option 1: The USITC REST API (free, US only)
The USITC publishes a REST API at hts.usitc.gov. It's real data and it's free. Here's how to use it:
Search by keyword
import requests
resp = requests.get(
"https://hts.usitc.gov/reststop/api/details/sectionJSON",
params={"query": "aluminum", "offset": 0, "limit": 20}
)
data = resp.json()
for item in data.get("HTSDetails", []):
print(item["htsno"], item["description"], item["general"])
Lookup by HTS code
resp = requests.get("https://hts.usitc.gov/reststop/api/details/htsnoJSON/0101.30.00.00")
detail = resp.json()
print(detail["htsno"]) # 0101.30.00.00
print(detail["description"]) # Asses
print(detail["general"]) # 6.8%
print(detail["special"]) # Free (A+,AU,BH,CL...)
Get an entire chapter
resp = requests.get("https://hts.usitc.gov/reststop/api/details/sectionJSON",
params={"query": "chapter:84", "limit": 500})
This works. The data is accurate. But there are real limitations you'll hit quickly.
Where the USITC API falls short
No Canadian data. The USITC API covers US imports only. If you need to show what a US exporter pays when shipping into Canada — the CUSMA/UST preferential rate, the MFN rate, the over-quota rate on dairy — you're on your own. The CBSA publishes the Canadian tariff as a PDF and an Excel file. There is no CBSA REST API.
No change detection. The USITC updates the HTS schedule — sometimes significantly, especially in a volatile tariff environment. The API doesn't tell you what changed. If you cache HTS data (and you should, for performance), you have no reliable signal for when to invalidate your cache.
Schema instability. The USITC has changed its API response format without notice. If you build a production integration directly on it, plan to babysit it.
No SLA. It's a government endpoint. It goes down. There's no uptime commitment.
Rate limits are unpublished. Hammer it and you'll get throttled, with no clear documentation on what the limits are.
For a side project or internal tool, these tradeoffs are often fine. For a production integration where your customers depend on rate accuracy, they're not.
Option 2: Parsing the CBSA Canadian Tariff yourself
The CBSA publishes the Canadian Customs Tariff as a downloadable Excel file each year. Here's how to parse it in Python:
import requests
import openpyxl
from io import BytesIO
url = "https://www.cbsa-asfc.gc.ca/trade-commerce/tariff-tarif/2026/html/tblmod-1-eng.xlsx"
resp = requests.get(url)
wb = openpyxl.load_workbook(BytesIO(resp.content), data_only=True)
ws = wb.active
records = []
for row in ws.iter_rows(min_row=2, values_only=True):
tariff_code, description, mfn_rate, ust_rate = row[0], row[1], row[3], row[8]
if tariff_code:
records.append({
"tariff": str(tariff_code),
"description": description,
"mfn": mfn_rate,
"ust": ust_rate,
})
print(f"Loaded {len(records)} records")
This works — once. The problems start when you need to maintain it:
- The CBSA changes the Excel column layout between releases without documentation
- The file URL changes each year
- Supply management goods (dairy, poultry) have over-quota rates of 200–400% that need special handling
- There are 6,229 duplicate records where
.00suffix variants exist alongside base codes - You're now responsible for storing, serving, and updating this data in your application
Option 3: Use a purpose-built tariff API
If you need both US HTS and Canadian Customs Tariff data with a stable schema and change detection, TradeFacts.io is a REST API built specifically for this. It pulls nightly from USITC and CBSA, normalizes both datasets into consistent JSON, diffs each nightly run to detect changes, and delivers webhook alerts when rates change.
import requests
headers = {"X-API-Key": "your_api_key"}
# US HTS lookup
resp = requests.get("https://tradefacts.io/api/hts/0101.30.00.00", headers=headers)
print(resp.json())
# Canadian tariff lookup
resp = requests.get("https://tradefacts.io/api/ca/0401.10.10", headers=headers)
print(resp.json())
# Search CA dataset
resp = requests.get("https://tradefacts.io/api/ca/search", params={"q": "dairy"}, headers=headers)
# What changed in the last 7 days?
resp = requests.get("https://tradefacts.io/api/changes", params={"days": 7}, headers=headers)
No-auth demo endpoint for evaluation:
curl "https://tradefacts.io/api/demo/search?q=steel&ds=us"
curl "https://tradefacts.io/api/demo/search?q=dairy&ds=ca"
30-day free trial at tradefacts.io — no credit card required.
Choosing the right approach
| USITC API | CBSA Excel | TradeFacts.io | |
|---|---|---|---|
| US tariff data | ✓ | ✗ | ✓ |
| Canadian tariff data | ✗ | ✓ (manual) | ✓ |
| Change detection | ✗ | ✗ | ✓ |
| Webhook alerts | ✗ | ✗ | ✓ (Pro) |
| Schema stability | ✗ | ✗ | ✓ |
| Uptime SLA | ✗ | ✗ | ✓ |
| Cost | Free | Free | Paid |
If you only need US data for a low-stakes integration: use the USITC API directly. If you need Canada, need both, or are building something production-critical: the maintenance cost of rolling your own will exceed the API cost fairly quickly.
Conclusion
Tariff data is genuinely messy infrastructure. The government sources are authoritative but not production-ready. If you're building something that needs to stay current as rates change — especially with the current pace of tariff policy changes — plan for that maintenance cost up front rather than discovering it six months in when a client's landed cost calculations are wrong.
Top comments (0)