Skip to content

Product report for an HS code

This guide builds a short market report for one 6-digit HS product from three API calls, using HS6 382440 (prepared binders and chemical mixtures: a chemicals code with Russian buyers, EU restrictions and enough trade data to be interesting) as the example. The script prints Markdown with the period or as_of next to every number.

The calls

Step Request What you take from it
1 GET /v1/products/382440?years=3 Russia's imports by supplier (annual and monthly mirror data), unit prices, Türkiye's exports (TÜİK), seasonality, China at chapter level, restrictions, and the buyers block
2 GET /v1/restrictions/382440 The full list of EU 833/2014 annex rows, the US CHP list result and the Russian parallel-import groups. The same restriction data is already inside step 1; this call is shown because it is the endpoint to use when you only need restrictions
3 GET /v1/companies/search?okved_prefix=20.59&size_band=large&only_active=true&limit=5 Sample companies in the OKVED class that buyers.by_okved ranks as relevant

The okved_prefix comes from the response of step 1: buyers.mappings says which OKVED classes buy this product and why, and buyers.by_okved gives the number of active companies in each. The script picks the largest class that is not wholesale (46.*), because wholesalers are intermediaries rather than end users; change that rule to suit your question.

Every response has meta.sources with source_id, fetched_at and as_of. The script merges them into one table at the end. See responses and sources for how to read them, and the endpoint pages for trade and product check, sanctions and restrictions and companies.

Script

Needs Python 3 and requests. Put your key in TRRU_KEY.

product_report.py
#!/usr/bin/env python3
"""Markdown report for one HS6 product. Usage: TRRU_KEY=... python product_report.py [HS6] [years]"""
import os
import sys
import time

import requests

BASE = "https://trru.atapinar.com"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['TRRU_KEY']}"


def get(path, **params):
    """GET with retry on 429/502/503/504. Returns (data, meta). Raises RuntimeError with the API error."""
    for attempt in range(4):
        r = SESSION.get(BASE + path, params=params, timeout=60)
        if r.status_code in (429, 502, 503, 504) and attempt < 3:
            time.sleep(min(int(r.headers.get("Retry-After", 2 ** attempt)), 30))
            continue
        break
    body = r.json()
    if r.status_code != 200:
        err = body.get("error", {})
        raise RuntimeError(f"{path}: HTTP {r.status_code} {err.get('code')}: {err.get('message')} "
                           f"(request id {r.headers.get('X-Request-Id')})")
    return body["data"], body["meta"]


def usd(v):
    return f"{v:,.0f}" if v is not None else "n/a"


def pct(v):
    return f"{v * 100:.1f}%"


def main():
    hs6 = sys.argv[1] if len(sys.argv) > 1 else "382440"
    years = int(sys.argv[2]) if len(sys.argv) > 2 else 3

    prod, meta_p = get(f"/v1/products/{hs6}", years=years)
    restr, meta_r = get(f"/v1/restrictions/{hs6}")

    # Merge sources from every call: source_id -> {fetched_at, as_of}
    sources = {}
    for meta in (meta_p, meta_r):
        for s in meta["sources"]:
            sources.setdefault(s["source_id"], s)
    as_of = lambda sid: (sources.get(sid) or {}).get("as_of") or "n/a"
    request_ids = [meta_p["request_id"], meta_r["request_id"]]

    out = [f"# HS {hs6}: market snapshot for Russia-bound trade", ""]

    # --- annual mirror: who supplies Russia
    annual = prod["russia_imports_by_supplier"]
    out += ["## Russia's imports by supplier (annual, partner-reported mirror data)", ""]
    if annual:
        out += ["| Year | Total USD | Top suppliers (share) |", "|---|---:|---|"]
        for y in annual:
            top = ", ".join(f"{s['reporter_iso2'] or s['reporter']} {pct(s['share'])}" for s in y["suppliers"][:3])
            out.append(f"| {y['year']} | {usd(y['total_value_usd'])} | {top} |")
        out += ["", f"Newest year in the table: {annual[-1]['year']}. The latest year may be reported by only some countries.", ""]
    else:
        out += [prod["notes"].get("russia_imports_by_supplier", "No data."), ""]

    # --- monthly mirror
    monthly = prod["monthly_suppliers"]
    if monthly:
        m = monthly[-1]
        top = m["suppliers"][0]
        out += [f"Latest month with data ({m['period'][:7]}, source comtrade_monthly, as_of {as_of('comtrade_monthly')}): "
                f"total USD {usd(m['total_value_usd'])}; largest reporter "
                f"{top['reporter_iso2'] or top['reporter']} with USD {usd(top['value_usd'])} ({pct(top['share'])}).", ""]

    # --- unit prices
    up = prod["unit_prices"]
    if up:
        out += [f"## Unit prices, {up['year']} (USD per kg, annual mirror data)", "", "| Supplier | USD/kg | Value USD |", "|---|---:|---:|"]
        for r in up["rows"][:5]:
            out.append(f"| {r['reporter_iso2'] or r['reporter']} | {r['usd_per_kg']:.2f} | {usd(r['value_usd'])} |")
        out.append("")

    # --- Türkiye's own exports to Russia
    tr = prod["turkey_exports_monthly"]
    out += ["## Türkiye's exports to Russia (TÜİK)", ""]
    if tr:
        last12 = tr[-12:]
        out += [f"- {last12[0]['period'][:7]} to {last12[-1]['period'][:7]} ({len(last12)} months with data): "
                f"USD {usd(sum(r['value_usd'] or 0 for r in last12))}, {usd(sum(r['net_kg'] or 0 for r in last12))} kg "
                f"(as_of {as_of('tuik_trade')}, source tuik_trade)."]
    else:
        out += [prod["notes"].get("turkey_exports_monthly", "No data."), ""]
    if prod["seasonality"]:
        best = sorted(prod["seasonality"]["months"], key=lambda m: -m["avg_share"])[:3]
        out += ["- Strongest months (average share of annual exports, years "
                + ", ".join(map(str, prod["seasonality"]["years"])) + "): "
                + ", ".join(f"month {m['month']} {pct(m['avg_share'])}" for m in best), ""]

    # --- China: chapter level only
    ch = prod["china_chapter_monthly"]
    if ch:
        last = ch["rows"][-1]
        out += [f"## China to Russia, HS chapter {ch['hs2']} (not this HS6)", "",
                f"{last['period'][:7]}: USD {usd(last['value_usd'])} for the whole chapter (source china_gacc, as_of {as_of('china_gacc')}). "
                f"{ch['note']}", ""]

    # --- restrictions
    out += ["## Restrictions", ""]
    out.append(f"- US Common High Priority list: {'yes' if restr['hs']['on_chp_list'] else 'no'} (source bis_chpl, list has no as_of date).")
    eu = restr["eu"]["eu_833_annexes"]
    if eu:
        for e in eu:
            out.append(f"- EU 833/2014 annex {e['annex']}, code {e['code']} ({e['match']}): {e['restriction']}, {e['article_ref']} "
                       f"(consolidated {e['consolidated_date']}).")
    else:
        out.append(f"- EU 833/2014 annexes: no match (eu_regs as_of {as_of('eu_regs')}).")
    pi = restr["eu"]["ru_parallel_import"]
    out.append(f"- Russian parallel-import list: {len(pi)} matching code group(s)"
               + (": " + ", ".join(f"{p['tnved_code']} ({p['mode']}, {p['rows']} rows)" for p in pi) if pi else "") + ".")
    out.append("")

    # --- buyers, then companies by OKVED prefix from the buyers block
    buyers = prod["buyers"]
    out += ["## Russian buyer base", ""]
    if buyers:
        notes = {m["okved_prefix"]: m["note"] for m in buyers["mappings"]}
        out += [f"Active companies per OKVED class (source most, as_of {as_of('most')}):", "",
                "| OKVED prefix | Active companies | Meaning |", "|---|---:|---|"]
        for b in buyers["by_okved"]:
            out.append(f"| {b['okved_prefix']} | {b['companies']:,} | {notes.get(b['okved_prefix'], '')} |")
        out += ["", "Top regions: " + ", ".join(f"{r['region']} ({r['companies']:,})" for r in buyers["top_regions"][:5]), ""]

        # Largest class that is not wholesale; fall back to the largest overall
        classes = sorted(buyers["by_okved"], key=lambda b: -b["companies"])
        pick = next((b for b in classes if not b["okved_prefix"].startswith("46")), classes[0])["okved_prefix"]
        found, meta_c = get("/v1/companies/search", okved_prefix=pick, size_band="large", only_active="true", limit=5)
        request_ids.append(meta_c["request_id"])
        for s in meta_c["sources"]:
            sources.setdefault(s["source_id"], s)
        out += [f"Sample of large active companies with OKVED {pick}* (as_of {as_of('most')}):", "",
                "| INN | Name | Region | OKVED | Revenue band | INN on a sanctions list |", "|---|---|---|---|---|---|"]
        for c in found["rows"]:
            out.append(f"| {c['inn']} | {(c['name_ru'] or c['name_en'] or '').replace('|', '/')} | {c['region']} | {c['okved']} | "
                       f"{c['revenue_band']} | {'yes' if c['sanctioned_inn'] else 'no'} |")
        out.append("")
    else:
        out += [prod["notes"].get("buyers", "No OKVED mapping for this HS code."), ""]

    # --- provenance
    out += ["## Sources", "", "| source_id | as_of | fetched_at |", "|---|---|---|"]
    for sid in sorted(sources):
        s = sources[sid]
        out.append(f"| {sid} | {s['as_of'] or 'n/a'} | {s['fetched_at']} |")
    out += ["", "Request ids: " + ", ".join(request_ids)]
    if prod["notes"]:
        out += ["", "Notes from the API:"] + [f"- {k}: {v}" for k, v in prod["notes"].items()]
    print("\n".join(out))


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as e:
        sys.exit(f"error: {e}")

Run it:

export TRRU_KEY=trru_YOUR_KEY
python product_report.py 382440 3 > report-382440.md

The arguments are the HS6 code (default 382440) and the number of annual years (default 3). A code that is not exactly 6 digits stops with error: ... bad_request: hs6 must be exactly 6 digits.

Output

Real output of the run above (2026-11-13, Cyrillic company names kept as the registry spells them):

# HS 382440: market snapshot for Russia-bound trade

## Russia's imports by supplier (annual, partner-reported mirror data)

| Year | Total USD | Top suppliers (share) |
|---|---:|---|
| 2023 | 26,187,087 | CN 31.4%, AM 22.4%, KZ 18.5% |
| 2024 | 23,561,642 | CN 36.0%, KZ 17.6%, AM 16.8% |
| 2025 | 13,130,742 | CN 55.4%, KZ 18.3%, IN 11.2% |

Newest year in the table: 2025. The latest year may be reported by only some countries.

Latest month with data (2026-08, source comtrade_monthly, as_of 2026-08-01): total USD 354,186; largest reporter IN with USD 177,039 (50.0%).

## Unit prices, 2025 (USD per kg, annual mirror data)

| Supplier | USD/kg | Value USD |
|---|---:|---:|
| CN | 1.42 | 7,275,197 |
| KZ | 5.26 | 2,405,363 |
| IN | 2.53 | 1,472,075 |
| GE | 1.47 | 1,002,980 |
| TR | 1.41 | 868,136 |

## Türkiye's exports to Russia (TÜİK)

- 2025-06 to 2026-07 (12 months with data): USD 879,851, 641,866 kg (as_of 2026-07-01, source tuik_trade).
- Strongest months (average share of annual exports, years 2020, 2021, 2024): month 9 13.0%, month 7 11.8%, month 6 11.6%

## China to Russia, HS chapter 38 (not this HS6)

2026-09: USD 135,124,000 for the whole chapter (source china_gacc, as_of 2026-09-01). China GACC publishes Russia trade by HS chapter (2-digit) only, so this is the value of the whole chapter, not of this HS6 line; use it for the direction of China's trade, not for the product's size.

## Restrictions

- US Common High Priority list: no (source bis_chpl, list has no as_of date).
- EU 833/2014 annex XXI, code 3824 (covers): import ban (revenue-generating goods), Art. 3i(1) (consolidated 2026-07-24).
- EU 833/2014 annex XXIII, code 3824 (covers): industrial-capacity goods export ban, Art. 3k(1) (consolidated 2026-07-24).
- Russian parallel-import list: 4 matching code group(s): 38 (brand, 47 rows), 38 (excluded, 3 rows), 3824400000 (brand, 9 rows), 3824400000 (excluded, 3 rows).

## Russian buyer base

Active companies per OKVED class (source most, as_of 2026-10-10):

| OKVED prefix | Active companies | Meaning |
|---|---:|---|
| 20.59 | 1,696 | prepared binders, chemical mixtures n.e.c.: other chemical products makers |
| 23.64 | 574 | concrete and mortar additives: dry construction mix makers |
| 46.75 | 10,566 | wholesale of chemical products |

Top regions: 77 Москва г (2,453), 78 Санкт-Петербург г (925), 52 НИЖЕГОРОДСКАЯ ОБЛАСТЬ (774), 50 МОСКОВСКАЯ ОБЛАСТЬ (756), 16 Татарстан Респ (713)

Sample of large active companies with OKVED 20.59* (as_of 2026-10-10):

| INN | Name | Region | OKVED | Revenue band | INN on a sanctions list |
|---|---|---|---|---|---|
| 4025450626 | ООО "ЮНАЙТЕД ПЕТРОКЕМИКАЛС" | 40 | 20.59.4 | gt_10b | no |
| 4707019370 | ООО "ПОЛИПЛАСТ СЕВЕРО-ЗАПАД" | 47 | 20.59.5 | gt_10b | no |
| 5506066492 | ООО "ОМСКТЕХУГЛЕРОД" | 55 | 20.59.50 | gt_10b | no |
| 6625021894 | ООО "ПОЛИПЛАСТ-УРАЛСИБ" | 66 | 20.59.50 | gt_10b | no |
| 0266032970 | ООО "САЛАВАТСКИЙ КАТАЛИЗАТОРНЫЙ ЗАВОД" | 02 | 20.59.50 | 1b_10b | no |

## Sources

| source_id | as_of | fetched_at |
|---|---|---|
| bis_chpl | n/a | 2026-11-13T06:17:05.185599+00:00 |
| china_gacc | 2026-09-01 | 2026-11-13T12:10:58.829621+00:00 |
| comtrade_monthly | 2026-08-01 | 2026-11-13T12:19:06.027831+00:00 |
| eu_fsf | n/a | 2026-11-13T06:15:29.885405+00:00 |
| eu_regs | 2026-07-24 | 2026-11-13T11:23:35.245395+00:00 |
| most | 2026-10-10 | 2026-11-12T22:46:29.632841+00:00 |
| ofac_sdn | n/a | 2026-11-13T06:16:10.754928+00:00 |
| parallel_import | n/a | 2026-11-13T10:47:35.313472+00:00 |
| tuik_trade | 2026-07-01 | 2026-11-13T11:48:32.012196+00:00 |
| uk_fcdo | n/a | 2026-11-13T06:16:45.866983+00:00 |
| un_sc | n/a | 2026-11-13T06:16:50.937380+00:00 |
| us_csl | n/a | 2026-11-13T06:17:04.741878+00:00 |

Request ids: 5a59f77c31034578857e85f92d8ba998, adfd3ed00fb242d983e6b9aa18b77eea, a3bca59e5f294805bd26e57097fc7334

How to read it

  • Annual versus monthly. The yearly table is built from what supplying countries report as exports to Russia; the monthly line comes from a separate monthly source, so the two do not add up to each other. The newest year or month may be reported by only some countries, which makes it look lower than it will be once the rest report. Compare years only when the supplier lists look complete.
  • China. Monthly China data after 2024 exist at HS-chapter level only. The script labels the figure with the chapter (38) and repeats the API's note: it shows the direction of China's trade in the chapter, not the size of this product.
  • Türkiye. The figure is the sum of the last 12 months with data, from TÜİK; months without data are not counted, and the script prints how many months it added.
  • Restrictions. An EU annex row with match: covers means the restriction applies to your code; narrower means only some subcodes are restricted. Quote the annex, the article and the consolidated_date. The US list and the parallel-import list carry no as_of date, so the script prints n/a rather than inventing one.
  • Buyers. The counts are active companies in the OKVED class, not importers of this product. Use them to size the market and to pick regions, then screen individual companies with the counterparty guide.
  • Unmapped products. The OKVED mapping exists for HS chapters 28 to 40 only. For other products buyers is null and the script prints the API's note instead of the company section; the other blocks still work (try 080810).

One call or three

/v1/products/{hs6} already contains the restrictions and the buyer counts. Call /v1/restrictions/{hs} separately only when you need nothing else, and /v1/companies/search when you want actual company rows. For an AI client the same dossier is one tool call: product_check.