This guide is for developers who have already decided they need an HTS tariff API and want to get data into their application as quickly as possible. It covers authentication, querying HTS codes, retrieving rate data, working with the change log, and setting up webhook delivery — with working code examples in Python and JavaScript.
In this guide:
1. Authentication
All requests require an API key passed in the X-API-Key header. Your key is issued when you request a trial — it arrives by email within a few minutes of submitting the form.
# Python import requests API_KEY = "your_api_key_here" BASE_URL = "https://tradefacts.io/api" headers = { "X-API-Key": API_KEY } resp = requests.get(f"{BASE_URL}/hts/0101.30.00.00", headers=headers) print(resp.status_code) # 200
// JavaScript (fetch)
const API_KEY = "your_api_key_here";
const BASE_URL = "https://tradefacts.io/api";
const resp = await fetch(`${BASE_URL}/hts/0101.30.00.00`, {
headers: { "X-API-Key": API_KEY }
});
const data = await resp.json();
A request without a key returns 401 Unauthorized. An expired trial key returns 403 Forbidden with a message directing you to upgrade. All other errors return standard HTTP status codes with a JSON body containing a detail field.
2. Looking Up a Single HTS Code
The most common operation: given an HTS number, retrieve the current rate data.
GET /api/hts/{htsno}
HTS numbers can be passed with or without dots. Both 0101.30.00.00 and 0101300000 resolve to the same record.
resp = requests.get(
f"{BASE_URL}/hts/8471.30.01.00",
headers=headers
)
print(resp.json())
The key rate fields are:
- general — the MFN (Most Favored Nation) rate, applied to imports from most countries
- special — preferential rates under free trade agreements; the letter codes identify which programs apply
- other — Column 2 rate, applied to non-market economy countries not granted MFN status
Note on the typo field: The USITC source data contains a misspelled field: addiitionalDuties (two i's). This is preserved as-is in our response to maintain schema fidelity with the source. Do not attempt to normalize it away — both additionalDuties and addiitionalDuties may contain data in different records.
3. Searching by Keyword
To find HTS codes by product description, use the search endpoint:
GET /api/hts/search?q={query}&limit={n}
resp = requests.get(
f"{BASE_URL}/hts/search",
headers=headers,
params={"q": "hydraulic pumps", "limit": 10}
)
for record in resp.json()["results"]:
print(record["htsno"], record["description"])
Search is case-insensitive and matches against the description field. The limit parameter defaults to 20 and caps at 100. Results are ordered by HTS number, not relevance — HTS descriptions are terse and technical, so relevance ranking adds little value over sequential ordering.
4. Fetching an Entire Chapter
For bulk operations — loading a chapter into a local database, building a dropdown, pre-computing classification options — use the chapter endpoint:
GET /api/hts/chapter/{chapter_number}
# Fetch all records in Chapter 84 (nuclear reactors, boilers, machinery)
resp = requests.get(
f"{BASE_URL}/hts/chapter/84",
headers=headers
)
chapter_data = resp.json()
print(f"{len(chapter_data['records'])} records in Chapter 84")
Chapter responses include a chapter field with the chapter number and a records array. Chapter 77 is intentionally empty — it is reserved by international agreement and contains no active codes. Chapter 99 contains Section 232, Section 301, and other additional duty provisions that reference other chapters; these are included in the response but warrant separate handling in most applications.
5. Working with the Change Log
The change log endpoint returns a history of every detected change to the tariff schedule since the service launched — added codes, removed codes, and modified codes with field-level diff detail.
GET /api/changes
resp = requests.get(f"{BASE_URL}/changes", headers=headers)
changes = resp.json()
for entry in changes:
print(entry["timestamp"], entry["change_count"], "changes")
Each change entry has a type field with one of three values:
- modified — the record exists in both old and new, with at least one field different. The entry includes both
oldandnewfull records. - added — the HTS code appears in the new dataset but not the old. The entry includes the
newrecord. - removed — the HTS code appears in the old dataset but not the new. The entry includes the
oldrecord.
To get only changes since a specific date, pass a since query parameter in ISO 8601 format:
resp = requests.get(
f"{BASE_URL}/changes",
headers=headers,
params={"since": "2026-02-01T00:00:00Z"}
)
To get changes for a specific HTS code only:
resp = requests.get(
f"{BASE_URL}/changes",
headers=headers,
params={"htsno": "9903.88.15"}
)
6. Webhook Delivery
Webhook delivery is available on Tier 2. Instead of polling the changes endpoint, you register an HTTPS endpoint and we POST change data to it whenever the nightly update detects modifications.
Register a webhook endpoint:
POST /api/webhooks
Content-Type: application/json
{
"url": "https://your-app.com/hooks/tariff-changes",
"secret": "your_signing_secret"
}
Each delivery is a POST with a JSON body matching the change log format. The request includes an X-TradeFacts-Signature header containing an HMAC-SHA256 signature of the request body, signed with your webhook secret. Always verify this signature before processing the payload:
# Python — verify webhook signature
import hmac, hashlib
def verify_signature(payload: bytes, header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", header)
// Express.js — verify webhook signature
const crypto = require("crypto");
function verifySignature(payload, header, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(header)
);
}
Deliveries are retried up to 3 times with exponential backoff if your endpoint returns a non-2xx status. A delivery that fails all retries is logged and available in the webhook delivery history endpoint.
7. Understanding the Data Schema
A few schema details that catch engineers on first integration:
The indent field. Each HTS record has an indent value from 0 to 9 that indicates its position in the hierarchical classification tree. Indent 0 is a chapter heading. Indent 1 is a heading (4-digit code). Higher indents are increasingly specific subheadings. When building a classification tree UI, you use indent to determine parent-child relationships between records, not the HTS number format alone.
Rates as strings, not numbers. The general, special, and other fields are strings, not numeric values. Rates can be ad valorem percentages ("6.8%"), specific duties ("$0.15/kg"), compound duties ("6.2% + $0.15/kg"), or "Free". Do not attempt to parse these into numbers without a proper rate parser that handles all formats.
The special field encoding. The special field combines the rate and the list of applicable programs in a single string: "Free (A+,AU,BH,CL,CO,D,E,IL,JO,KR,MA,OM,P,PA,PE,S,SG)". The letter codes map to specific trade programs — CA is USMCA, AU is the US-Australia FTA, and so on. The full mapping is in our API documentation.
Null vs empty string. Some fields return null when not applicable, others return an empty string. The quotaQuantity and additionalDuties fields return null when absent. The general field returns an empty string for header rows that have no rate. Check for both when testing for absence of a value.
8. Error Handling
All error responses follow a consistent format:
Status codes you will encounter:
200— success400— bad request (malformed HTS number, invalid parameter)401— missing API key403— expired trial key; contact [email protected] to upgrade404— HTS code not found in current dataset429— rate limit exceeded500— server error; retry with backoff
Rate limits are generous for typical integration use. If you are bulk-loading data for the first time, use the chapter endpoints rather than individual lookups — one request per chapter rather than thousands of individual requests is faster and well within limits.
Need something not covered here? The full API reference is at tradefacts.io/docs. If you run into something undocumented or unexpected, email [email protected] — questions from developers in active integration get fast responses.
30-day free trial, no credit card required. Request access and you will have an API key in your inbox within minutes. The trial includes full access to all endpoints, the complete HTS dataset, and the change log history.