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.

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())
{ "htsno": "8471.30.01.00", "description": "Portable automatic data processing machines, weighing not more than 10 kg", "indent": "2", "general": "Free", "special": "Free (A,AU,BH,CA,CL,CO,D,E,IL,JO,KR,MA,MX,OM,P,PA,PE,S,SG)", "other": "35%", "units": ["No."], "footnotes": [], "quotaQuantity": null, "additionalDuties": null, "addiitionalDuties": null }

The key rate fields are:

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.

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")
[ { "timestamp": "2026-02-14T02:01:18.443Z", "record_count_old": 32289, "record_count_new": 32295, "change_count": 12, "changes": [ { "type": "modified", "htsno": "9903.88.15", "old": { "general": "25%", ... }, "new": { "general": "30%", ... } }, ... ] } ]

Each change entry has a type field with one of three values:

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:

{ "detail": "HTS code not found: 9999.99.99.99" }

Status codes you will encounter:

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.

Get your API key

30-day free trial. Full dataset. No credit card required.

Request API Access