Canada Border Services Agency (CBSA) publishes the Canadian Customs Tariff schedule annually. It covers 22,000+ tariff lines across 97 chapters, including MFN rates, preferential rates under CUSMA (formerly NAFTA), and over-quota rates for supply-managed goods. If you’re building cross-border trade software, ERP integrations, or customs compliance tools that touch Canadian imports, you need this data in machine-readable form.
The problem: CBSA doesn’t offer a developer API. The official source is a PDF and a set of Excel files — not something you can query programmatically. This guide covers the data structure, what each field means, and how to access the full schedule via the TradeFacts.io API with working code examples.
In this guide:
- How Canadian tariff codes differ from US HTS codes
- The Canadian tariff data structure
- Understanding MFN, UST, and preferential rates
- Looking up a Canadian tariff code
- Searching by keyword
- Fetching a full chapter
- Filtering by UST (CUSMA) rate
- Tracking Canadian tariff changes
- Data quirks and structural nodes
1. How Canadian Tariff Codes Differ from US HTS Codes
Both Canada and the US use the Harmonized System (HS) as a foundation, so the first six digits of a tariff code are internationally standardized. But the two schedules diverge at the national level:
| US HTS | Canadian Customs Tariff | |
|---|---|---|
| Format | 10 digits (e.g. 8471.30.01.00) | 8–10 digits (e.g. 8471.30.00.10) |
| Authority | USITC | CBSA / Finance Canada |
| Trade agreement | USMCA / FTA special rates | CUSMA (same agreement, Canadian name) |
| Preference column name | Special | UST (US Tariff) in preferential{} |
| Supply management | Chapter 99 over-quota | Embedded in MFN / general fields (200–400%) |
| Structural nodes | Present (heading-level rows) | Present (e.g. ‘04.05’ with no rates) |
A critical practical difference: the Canadian schedule uses a preferential object to hold trade agreement rates, keyed by program code (UST, CCCT, GPT, etc.), rather than a single special string like the US schedule. This matters for how you write your rate resolution logic.
2. The Canadian Tariff Data Structure
Each record returned by the TradeFacts Canadian tariff API has this shape:
All field names are lowercase. Here’s what each field means:
| Field | Type | Description |
|---|---|---|
tariff | string | The Canadian tariff classification number |
description | string | Product description from the official schedule |
mfn | string | Most Favoured Nation rate — the default rate for WTO members |
general | string | General (non-MFN) rate, applied to non-WTO countries |
uom | string | Unit of measure (e.g. KG, L, No.) — empty string if not applicable |
eff_date | string | Effective date of this rate (YYYY-MM-DD) |
footnote | string | Footnote code from the schedule — empty if none |
preferential | object | Trade agreement rates keyed by program code |
3. Understanding MFN, UST, and Preferential Rates
The rate field you care about depends on your use case:
MFN rate (mfn) is the standard rate applied to imports from WTO member countries, including the US under normal trade conditions. This is your baseline. If a record shows "mfn": "Free", the good enters Canada duty-free for all MFN countries.
UST rate (preferential.UST) is the rate applicable to US-origin goods under CUSMA (the Canada-United States-Mexico Agreement, Canada’s name for USMCA). If your software is helping importers of US-origin goods into Canada, this is almost always the rate you want to display. Goods qualifying under CUSMA rules of origin typically enter at "Free" across most categories.
General rate (general) applies to imports from countries not granted MFN status — essentially a punitive rate for non-WTO trading partners. For most commercial applications, you can ignore this field.
Supply management goods (dairy, eggs, poultry) are the exception to “Free under CUSMA.” These categories carry within-quota rates as low as 0–7.5% but over-quota MFN rates of 200–400%. The TRS (Tariff Rate Quota) system governs access. If your application touches HS chapters 02, 04, or 07, always surface the mfn rate alongside the UST rate — the gap is significant.
4. Looking Up a Canadian Tariff Code
Retrieve a single record by tariff code:
GET /api/ca/{tariff_code}
# 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}/ca/8471.30.00.10", headers=headers)
record = resp.json()
print(f"MFN rate: {record['mfn']}")
print(f"UST rate (CUSMA): {record['preferential'].get('UST', 'N/A')}")
# curl
curl https://tradefacts.io/api/ca/8471.30.00.10 \
-H "X-API-Key: your_api_key_here"
Important: The Canadian lookup endpoint is
/api/ca/{code}— not/api/ca/tariff/{code}. The latter returns 404. This is a common integration mistake when switching from the US endpoint pattern.
Tariff codes can be passed with or without dots. 8471.30.00.10 and 8471300010 resolve to the same record. Codes shorter than 10 digits are also valid — many Canadian codes are 8 digits.
5. Searching the Canadian Schedule by Keyword
To find tariff codes by product description:
GET /api/ca/search?q={query}
resp = requests.get(
f"{BASE_URL}/ca/search",
headers=headers,
params={"q": "aluminum flat-rolled"}
)
for record in resp.json()["results"]:
ust = record.get("preferential", {}).get("UST", "—")
print(f"{record['tariff']} MFN: {record['mfn']} UST: {ust}")
The response includes a count field with the total number of matching records and a results array. The search matches against the description field and is case-insensitive. Like the US endpoint, results are ordered by tariff code rather than relevance.
6. Fetching a Full Canadian Chapter
To retrieve all records for a chapter — useful for pre-loading data or building classification tools:
GET /api/ca/chapter/{chapter_prefix}
# Fetch all records in chapter 84 (machinery)
resp = requests.get(
f"{BASE_URL}/ca/chapter/84",
headers=headers
)
records = resp.json()["results"]
print(f"Chapter 84: {len(records)} records")
The chapter prefix is the two-digit HS chapter number. For chapters with sub-prefixes, you can also pass a four-digit heading (e.g. 8471) to retrieve just that heading’s records.
7. Filtering by UST Rate
One endpoint unique to the Canadian schedule: retrieve all records that carry a UST (CUSMA) preferential rate. This is useful for compliance dashboards showing US-origin goods, or for building CUSMA benefit calculators:
GET /api/ca/ust
resp = requests.get(f"{BASE_URL}/ca/ust", headers=headers)
ust_records = resp.json()["results"]
# Find all records where UST rate is "Free"
free_ust = [
r for r in ust_records
if r.get("preferential", {}).get("UST") == "Free"
]
print(f"{len(free_ust)} records with UST = Free")
Note that this endpoint returns only records that have a UST entry in their preferential object — records where UST is absent (i.e. not applicable to US-origin goods) are excluded from the response.
8. Tracking Canadian Tariff Changes
CBSA updates the Canadian tariff schedule periodically — typically at the start of the calendar year, and sometimes mid-year when trade agreements are amended or tariff rate quotas are adjusted. The TradeFacts change log captures these diffs nightly:
GET /api/ca/changelog
resp = requests.get(f"{BASE_URL}/ca/changelog", headers=headers)
changes = resp.json()["changes"]
for change in changes[:5]:
print(f"{change['tariff']} | {change['field']} | "
f"{change['old_value']} -> {change['new_value']} | {change['detected']}")
Each entry in the changelog records the affected tariff code, which field changed (typically mfn, a preferential rate key, or description), the old and new values, and the date the change was detected. This is particularly relevant in the current environment of active CUSMA negotiations and supply management reform — a UST rate change on a dairy product can materially affect compliance calculations with no advance notice from CBSA.
For real-time push notification when Canadian rates change, webhook delivery is available on the Pro plan.
9. Data Quirks and Structural Nodes
Two things to handle gracefully in your integration:
Structural heading nodes. The Canadian schedule includes organizational rows — entries like 04.05 or 8486 that represent category headings, not actual tariff lines. These records have empty or null rate fields. They exist to preserve the hierarchical structure of the schedule. Filter them out if you’re displaying only actionable tariff lines:
# Filter out structural nodes (no MFN rate)
active_records = [r for r in records if r.get("mfn") not in ("", None)]
The .00 duplicate issue. CBSA publishes some codes in both their base form (e.g. 0401.10.10) and with a .00 suffix (0401.10.10.00). These are the same record. The full API returns both as published by CBSA — accurate to the source. The demo endpoint deduplicates them. In your own application, you may want to normalize codes to their base form before storing:
# Normalize .00 suffix variants
def normalize_ca_code(code):
if code.endswith(".00"):
return code[:-3]
return code
Total record count: The full Canadian schedule contains 22,461 records. Of these, approximately 6,229 are .00 suffix duplicates of base codes. After deduplication, the working dataset is roughly 16,232 unique tariff lines.
Start querying Canadian tariff data today
30-day free trial. All Canadian and US endpoints included. API key in your inbox within minutes.
Request Free TrialAlso see: Canada Customs Tariff API overview · US HTS Quick Start Guide