Skip to main content
Data & Code/API Access/Comtrade API

Comtrade API

Overview

UN Comtrade is the primary source for bilateral trade statistics — it covers most countries, goes back to the early 1960s, and disaggregates to the 6-digit HS level. The main friction is the API layer: rate limits, authentication changes, and shifting endpoint conventions mean the scraping code needs updating every few years.

This page documents two distinct workflows. The modern approach (2025 onwards) uses the official comtradeapicall Python library to bulk-download full annual files by reporter and year, which scales much better for multi-country panel construction. The legacy approach (2021–2024) called the free REST endpoint from within Stata using embedded Python — no subscription required but capped at 100 requests per hour.

The legacy scripts have circulated fairly widely — several people reached out after the original blog post — so they are preserved here with personal paths and network-specific comments cleaned up. The modern workflow was developed for intra-Africa trade analysis and African petroleum import tracking.

Modern workflow · 2025 onwards

Python · comtradeapicall bulk download

Context

Comtrade's new API (v2 / Comtrade Plus) restructured authentication around subscription keys and introduced a dedicated bulk-download endpoint that returns pre-compiled tab-delimited files, one per reporter × year. The official comtradeapicall library wraps both the preview (free) and bulk (subscription) endpoints cleanly.

The workflow below was built for constructing long intra-Africa trade panels and African petroleum import matrices. Each bulk file contains the full HS commodity breakdown for a single reporter × year, so you download once and filter locally — much more efficient than making one API call per commodity.

UN staff have access through an institutional licence, some universities too, e.g. Geneva Graduate Institute. External researchers can obtain a key by registering a profile at comtradeplus.un.org. The key is not included in the snippets below; insert your own where indicated.

Setup & reporter list

Imports, a simple Excel-export helper, and the dictionary of all 54 African UN numeric codes used as the reporter list throughout the analysis.

Python · imports & African reporter codes

All 54 African UN numeric codes; reusable across every step below.

Python
comtrade_setup.py
python
1# Install the official wrapper once
2# pip install comtradeapicall
3
4import comtradeapicall
5import pandas as pd
6import os
7import time
8from datetime import datetime
9
10today = datetime.now().strftime('%d%b%Y')
11
12def output_to_excel(df, filename):
13 path = f'./{filename}_{today}.xlsx'
14 df.to_excel(path, index=False)
15 print(f"Saved: {path}")
16
17# ── Reporter list: all 54 African UN numeric codes ───────────────────────────
18reporter_iso3 = {
19 12: "Algeria", 24: "Angola", 204: "Benin", 72: "Botswana",
20 854: "Burkina Faso", 108: "Burundi", 132: "Cape Verde", 120: "Cameroon",
21 140: "CAR", 148: "Chad", 174: "Comoros", 178: "Congo",
22 384: "Côte d'Ivoire", 262: "Djibouti", 818: "Egypt", 232: "Eritrea",
23 231: "Ethiopia", 226: "Eq. Guinea", 266: "Gabon", 270: "Gambia",
24 288: "Ghana", 324: "Guinea", 624: "Guinea-Bissau", 404: "Kenya",
25 426: "Lesotho", 430: "Liberia", 434: "Libya", 450: "Madagascar",
26 454: "Malawi", 466: "Mali", 478: "Mauritania", 480: "Mauritius",
27 508: "Mozambique", 504: "Morocco", 516: "Namibia", 562: "Niger",
28 566: "Nigeria", 646: "Rwanda", 678: "São Tomé", 686: "Senegal",
29 690: "Seychelles", 694: "Sierra Leone", 706: "Somalia",
30 710: "South Africa", 728: "South Sudan", 729: "Sudan",
31 748: "Eswatini", 834: "Tanzania", 768: "Togo", 788: "Tunisia",
32 800: "Uganda", 894: "Zambia", 716: "Zimbabwe",
33}
34reporters = list(reporter_iso3.keys())

Bilateral flows · no subscription key needed

The previewFinalData() endpoint is publicly accessible without a subscription key and is the recommended starting point for most users. The trade-off is a hard cap of 500 rows per call, which makes it well-suited for aggregate or HS-2 level queries but impractical for full HS-6 breakdowns across many partners.

The example below pulls each of the 54 African countries' total imports from China across 2015–2023 — a clean bilateral panel that fits comfortably within the row limit. Looping over reporters rather than commodities is the right pattern here.

Credit: github.com/uncomtrade/comtradeapicall — the official UN Comtrade Python wrapper. Install with pip install comtradeapicall.

Python · 54 African reporters × imports from China

No subscription key required. Uses the preview endpoint; loops over all African reporters with a 1-second sleep between calls.

Python
comtrade_africa_imports_china.py
python
1# ── African countries' imports from China, looping over all 54 reporters ────
2# Uses comtradeapicall.previewFinalData() — NO subscription key required.
3# Limit: 500 rows per call. Suitable for aggregate or HS-2 level queries.
4# For full HS detail across all partners, a subscription key is needed (see below).
5#
6# Credit: github.com/uncomtrade/comtradeapicall
7
8import comtradeapicall
9import pandas as pd
10import time
11from datetime import datetime
12
13# reporter_iso3 and reporters defined in setup above
14
15results = []
16
17for reporter in reporters:
18 df = comtradeapicall.previewFinalData(
19 typeCode = 'C', # Commodities
20 freqCode = 'A', # Annual
21 clCode = 'HS', # HS classification
22 reporterCode = reporter,
23 partnerCode = 156, # 156 = China
24 partner2Code = None,
25 customsCode = None,
26 motCode = None,
27 period = '2015,2016,2017,2018,2019,2020,2021,2022,2023',
28 cmdCode = 'TOTAL', # TOTAL = all commodities combined
29 flowCode = 'M', # M = imports
30 maxRecords = 500,
31 format_output= 'JSON',
32 countOnly = None,
33 includeDesc = True
34 )
35 if df is not None and not df.empty:
36 results.append(df)
37 time.sleep(1) # respect rate limit
38
39panel = pd.concat(results, ignore_index=True)
40panel.to_excel(f'Africa_M_from_CHN_{datetime.now().strftime("%d%b%Y")}.xlsx', index=False)
41print(panel[['period','reporterDesc','partnerDesc','cmdCode','primaryValue']].head(10))

Bulk download · subscription key required

For full-matrix work — all commodities, all partners, across all reporters and years — the bulkDownloadFinalFile() endpoint is the only practical option. It returns pre-compiled tab-delimited files (one per reporter × year) that can be filtered locally, bypassing the per-call row limit entirely.

Subscription required. This endpoint requires a paid Comtrade Plus key. UN staff can obtain one through their institution's licence. External users can subscribe at comtradeplus.un.org. The key in the snippet below is a placeholder — replace it with your own.

Iterates over every year from 1991 to 2024 and every African reporter. Expect around 2 000–2 500 files for the full African panel; download time varies with connection speed and key tier.

Python · bulk file download loop

Downloads one file per reporter × year. Replace the placeholder with your own subscription key.

Python · subscription
comtrade_bulk_download.py
python
1# ── Bulk download: one file per reporter × year ─────────────────────────────
2# Requires a UN Comtrade subscription key.
3# UN staff: obtain your key through your institution's Comtrade licence.
4# External users: subscribe at comtradeplus.un.org
5
6SUBSCRIPTION_KEY = "YOUR_SUBSCRIPTION_KEY_HERE"
7
8for period in range(1991, 2025):
9 for reporter in reporters:
10 comtradeapicall.bulkDownloadFinalFile(
11 SUBSCRIPTION_KEY,
12 './input/Comtrade_bulk',
13 typeCode = 'C', # Commodities
14 freqCode = 'A', # Annual
15 clCode = 'HS', # HS classification
16 period = period,
17 reporterCode= reporter,
18 decompress = True # unzip .gz automatically
19 )
20 time.sleep(0.5) # be polite to the server

Intra-Africa extraction

Reads every downloaded file and filters to export flows where the partner is another African country (or World, for total export benchmarking). The resulting panel is one row per reporter × partner × year and exported to Excel with a date stamp.

Python · build intra-Africa trade panel

Loops over bulk files, filters to African partners, concatenates.

Python · subscription
comtrade_intra_africa.py
python
1# ── Extract intra-Africa trade from bulk files ───────────────────────────────
2file_path = './input/Comtrade_bulk/'
3results = {}
4reporters_plus_world = reporters + [0] # 0 = World aggregate
5
6for filename in os.listdir(file_path):
7 if not filename.endswith('.txt'):
8 continue
9
10 full_path = os.path.join(file_path, filename)
11 df_raw = pd.read_csv(full_path, delimiter='\t', header=0, dtype={14: str})
12
13 # Keep only required columns
14 cols = ['period', 'reporterCode', 'flowCode', 'partnerCode', 'cmdCode', 'primaryValue']
15 df = df_raw[cols].copy()
16
17 # Filter: exports (X) to African partners or World
18 df_filtered = df[
19 (df['flowCode'] == 'X') &
20 (df['partnerCode'].isin(reporters_plus_world))
21 ]
22
23 key = filename.replace('.txt', '')
24 results[key] = df_filtered
25
26# Concatenate and export
27final_df = pd.concat(results.values(), ignore_index=True)
28output_to_excel(final_df, 'Comtrade_IntraAfrica')

Energy trade (HS 27)

Extracts both import and export flows, keeping only TOTAL and HS chapter 27 (mineral fuels, oils). This produces a bilateral energy trade matrix across African reporters — the foundation for petroleum import dependency and intra-Africa energy flow analysis.

Python · imports & exports, TOTAL + HS 27

Keeps both flow directions; commodity filter: TOTAL and mineral fuels.

Python · subscription
comtrade_energy_hs27.py
python
1# ── Extract petroleum trade (HS 27) alongside total trade ───────────────────
2results = {}
3cmd_keep = ['TOTAL', '27'] # TOTAL = all commodities; 27 = mineral fuels
4
5for filename in os.listdir(file_path):
6 if not filename.endswith('.txt'):
7 continue
8
9 full_path = os.path.join(file_path, filename)
10 df_raw = pd.read_csv(full_path, delimiter='\t', header=0, dtype={14: str})
11
12 cols = ['period', 'reporterCode', 'flowCode', 'partnerCode', 'cmdCode', 'primaryValue']
13 df = df_raw[cols].copy()
14
15 df_filtered = df[
16 (df['flowCode'].isin(['M', 'X'])) &
17 (df['partnerCode'].isin(reporters_plus_world)) &
18 (df['cmdCode'].isin(cmd_keep))
19 ]
20
21 results[filename.replace('.txt', '')] = df_filtered
22
23final_df = pd.concat(results.values(), ignore_index=True)
24output_to_excel(final_df, 'Comtrade_M_X_TOTAL_27')
Legacy workflow · 2021–2024

Stata + embedded Python

Context

Before Comtrade Plus launched (and before the free API was effectively deprecated), the public REST endpoint at comtrade.un.org/api/get returned JSON without any authentication. The rate limit was generous enough for most academic use if you added a short sleep between calls.

The approach below embeds Python inside a Stata do-file using the python: block introduced in Stata 16. Results were saved directly as .dta files, which Stata could then append and analyse without any intermediate conversion step. Practically useful if your downstream analysis was already Stata-based.

Note: The free REST endpoint is now heavily throttled or unavailable for bulk use. These scripts are preserved for reference; for new work, see the modern workflow above.

Annual · one partner (China)

The simplest variant: loop over years, pull all reporters' exports to a single partner (here, China). One .dta file per year lands in the output folder.

Stata · annual exports to China

Fetches all reporters × years, partner fixed to China (UN code 156).

Stata / Python
comtrade_api.do
stata
1clear all
2*! Stata + Python integration — wraps the legacy Comtrade REST API
3* Requires on-site network or institutional VPN (rate-limit: 100 req/hr for guests)
4* Credit: @Satyam · https://blog.stata.com/2020/09/29/stata-python-integration-part-6-working-with-apis-and-json-data/
5
6local filepath = "/path/to/your/output/folder"
7cd `filepath'
8
9python:
10import json
11import numpy as np
12import pandas as pd
13import requests
14
15def Comtrade_Scraper(ps: int,
16 type: str = 'C',
17 freq: str = 'A',
18 px: str = 'S2',
19 r: str = 'all',
20 p: int = 156, # 156 = China (UN numeric)
21 rg: int = 2, # 2 = exports
22 cc: str = 'AG2'): # AG2 = 2-digit HS aggregation
23 """
24 Fetch one year of trade data from the legacy Comtrade API and save as .dta
25 """
26 base = 'https://comtrade.un.org/api/get?max=10000'
27 url = f'{base}&type={type}&freq={freq}&px={px}&ps={ps}&r={r}&p={p}&rg={rg}&cc={cc}'
28 result = requests.get(url).json()
29
30 if 'dataset' in result:
31 df = pd.DataFrame(result['dataset'])
32 df = df.replace({None: np.nan})
33 df.columns = [c[:32] for c in df.columns] # Stata column-name limit
34 df.to_stata(f'i_X_CHN_{ps}.dta')
35 return df
36
37# Loop over years — note: the last value in range() is exclusive
38for yr in range(2000, 2022):
39 Comtrade_Scraper(yr)
40end

Monthly · all reporters → World

Switches to monthly frequency (freq='M') and sets partner to World (code 0). After the loop, a Stata macro appends all yearly files into a single panel.

Stata · monthly total exports (2019–2022)

Monthly panel, all reporters, partner = World, TOTAL commodity class.

Stata / Python
i_X_j_monthly.do
stata
1clear all
2*! Monthly trade flows — all reporters, all partners, TOTAL commodity
3* Uses legacy API endpoint; rate limit still applies
4
5if "`c(username)'" == "your_username" ///
6 local filepath = "/path/to/your/project/BBL_demo"
7
8cd `filepath'/output/i_X_WLD_monthly
9
10python:
11import numpy as np
12import pandas as pd
13import requests
14
15def Comtrade_Scraper(ps: int,
16 type: str = 'C',
17 freq: str = 'M', # M = monthly
18 px: str = 'HS',
19 p: int = 0, # 0 = World
20 r: str = 'all',
21 rg: int = 2,
22 cc: str = 'TOTAL'):
23
24 base = 'https://comtrade.un.org/api/get?max=10000'
25 url = f'{base}&type={type}&freq={freq}&px={px}&ps={ps}&r={r}&p={p}&rg={rg}&cc={cc}'
26 result = requests.get(url).json()
27
28 if 'dataset' in result:
29 df = pd.DataFrame(result['dataset'])
30 df = df.replace({None: np.nan})
31 df.columns = [c[:32] for c in df.columns]
32 df.to_stata(f'i_X_WLD_{ps}_monthly.dta')
33 return df
34
35for yr in range(2019, 2023):
36 Comtrade_Scraper(yr)
37end
38
39*── Append all annual files into one panel ──────────────────────────────────
40di "`c(pwd)'"
41local files : dir "`c(pwd)'" files "*.dta"
42foreach x of local files {
43 append using `x'
44}
45save ./i_X_WLD_monthly.dta, replace
46tab period

Partner list loop

When you only want flows to a defined list of partners rather than the full matrix, pass a Python list of country names and let country_converter translate them to UN numeric codes. This hits the 100 000-row limit on institutional networks; on guest access the ceiling drops to 10 000.

Stata · bilateral flows, partner list

Loops over year × partner pairs; uses country_converter for name→code conversion.

Stata / Python
i_X_list.do
stata
1clear all
2*! Loop over a list of partner countries using country_converter
3* Useful when you only need bilateral flows, not "all partners"
4
5python:
6import numpy as np
7import pandas as pd
8import requests
9import country_converter as cc # pip install country_converter
10
11def Comtrade_Scraper(start_yr: int = 2019,
12 end_yr: int = 2022,
13 type: str = 'C',
14 freq: str = 'A',
15 px: str = 'S2',
16 r: str = 'all',
17 p: list = ["China", "Chile"],
18 rg: int = 2,
19 cc_code: str = 'AG2'):
20
21 # Convert country names → UN numeric codes
22 p_codes = cc.convert(p, to='UN')
23
24 for partner in p_codes:
25 for yr in range(start_yr, end_yr):
26 base = 'https://comtrade.un.org/api/get?max=100000'
27 url = f'{base}&type={type}&freq={freq}&px={px}&ps={yr}&r={r}&p={partner}&rg={rg}&cc={cc_code}'
28 result = requests.get(url).json()
29
30 if 'dataset' in result:
31 df = pd.DataFrame(result['dataset'])
32 df = df.replace({None: np.nan})
33 df.columns = [c[:32] for c in df.columns]
34 df.to_stata(f'i_X_{partner}_{yr}.dta')
35
36Comtrade_Scraper()
37end

Notes & gotchas

Off-by-one in year ranges

Python's range(2000, 2022) stops at 2021. In the original scripts this caused silent gaps at the end of the loop. Always verify your coverage with a quick df['period'].unique() after concatenation.

Rate limits & network tier

Legacy API: guest accounts were capped at 10 000 rows per call; the 100 000 row limit required institutional on-site access or the right VPN profile. Modern API: bulk files bypass row limits but count against daily download quotas — add a short sleep between calls for courtesy.

Stata column-name truncation

Stata variable names are limited to 32 characters. The legacy scripts trim DataFrame column names with df.columns = [c[:32] for c in df.columns] before saving as .dta. Skip this if you are staying in Python.

HS revision discontinuities

Comtrade stores data under the classification declared by the reporter. Long panels spanning HS 1988, 1996, 2002, 2007, 2012, and 2017 revisions will have code-level breaks. For aggregate analysis (TOTAL or AG2), this is usually not an issue; for 6-digit product-level work, check the revision field and concordance accordingly.

Missing reporters in bulk files

Not every African country reports every year. The bulk download call returns an empty result (or no file) for missing reporter × year combinations — the loop handles this silently. Validate panel completeness by checking which reporter × year pairs appear in the concatenated output.