Code examples (curl, Python, JavaScript)¶
Ten common calls, each in curl, Python and JavaScript, followed by a small reusable client for retries, envelope handling and source collection. All snippets read the key from the environment variable TRRU_KEY:
- Python snippets need Python 3 and
requests(pip install requests). - JavaScript snippets need Node 18 or newer (global
fetch) and use top-levelawait: save them asexample.mjsand runnode example.mjs. - Every success response is
{"data": ..., "meta": {"sources": [...], "request_id": "...", "truncated": false}}; every error is{"error": {"code": "...", "message": "..."}}. See responses and sources and errors. The snippets print the useful part ofdata; the clients at the end unwrap the envelope for you. - The API is for server-side use (no CORS headers), so do not call it from browser code.
The shortest possible call, to check your key:
1. Series history¶
GET /v1/series/{series_id}: observations between start and end, optionally resampled to week, month, quarter or year. More: series.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/series/ru.fx.usdrub",
params={"start": "2026-01-01", "resample": "month"},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
body = r.json()
for row in body["data"]["rows"]:
print(row["period"], row["obs_date"], row["value"])
print(body["meta"]["sources"])
const params = new URLSearchParams({ start: "2026-01-01", resample: "month" });
const res = await fetch(`https://trru.atapinar.com/v1/series/ru.fx.usdrub?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const body = await res.json();
for (const row of body.data.rows) console.log(row.period, row.obs_date, row.value);
console.log(body.meta.sources);
2. Latest values¶
GET /v1/latest?ids=a,b,c: newest observation per series with its as_of. Ids that do not exist are absent from rows and are listed under data.missing, so a typo is visible instead of silently dropped.
import os
import requests
ids = ["ru.key_rate", "tr.fx.usdtry", "ru.fx.usdrub"]
r = requests.get(
"https://trru.atapinar.com/v1/latest",
params={"ids": ",".join(ids)},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
rows = r.json()["data"]["rows"]
for row in rows:
print(row["series_id"], row["value"], row["unit"], "as_of", row["as_of"])
print("not returned:", sorted(set(ids) - {row["series_id"] for row in rows}))
const ids = ["ru.key_rate", "tr.fx.usdtry", "ru.fx.usdrub"];
const res = await fetch(`https://trru.atapinar.com/v1/latest?ids=${ids.join(",")}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
for (const row of data.rows) console.log(row.series_id, row.value, row.unit, "as_of", row.as_of);
const got = new Set(data.rows.map((r) => r.series_id));
console.log("not returned:", ids.filter((id) => !got.has(id)));
3. Company by INN¶
GET /v1/companies/{inn}: the registry card with structure, address and sanctions screening. A malformed INN (not 10 or 12 digits) is 400 bad_request; a well-formed unknown INN is 404 not_found. More: companies.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/companies/7708503727",
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
data = r.json()["data"]
c = data["company"]
print(c["name_ru"], c["inn"], c["okved"], c["status"], "as_of", c["as_of"])
print("INN hits on sanctions lists:", len(data["sanctions"]["inn_hits"]))
const res = await fetch("https://trru.atapinar.com/v1/companies/7708503727", {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
const c = data.company;
console.log(c.name_ru, c.inn, c.okved, c.status, "as_of", c.as_of);
console.log("INN hits on sanctions lists:", data.sanctions.inn_hits.length);
4. Company search¶
GET /v1/companies/search: fuzzy name, INN, OKVED prefix, region, size and revenue band. At least one filter is required. Non-ASCII values must be URL-encoded; curl -G --data-urlencode and the Python and JavaScript helpers do this.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/companies/search",
params={"okved_prefix": "20.1", "region": "Татарстан", "only_active": "true", "limit": 3},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
for c in r.json()["data"]["rows"]:
print(c["inn"], c["name_ru"], c["okved"], c["size_band"], "sanctioned_inn:", c["sanctioned_inn"])
const params = new URLSearchParams({ okved_prefix: "20.1", region: "Татарстан", only_active: "true", limit: "3" });
const res = await fetch(`https://trru.atapinar.com/v1/companies/search?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
for (const c of (await res.json()).data.rows) {
console.log(c.inn, c.name_ru, c.okved, c.size_band, "sanctioned_inn:", c.sanctioned_inn);
}
5. Sanctions check¶
GET /v1/sanctions/check: INN matched exactly, name matched fuzzily. Candidates, not findings. More: sanctions and restrictions and the counterparty guide.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/sanctions/check",
params={"name": "Sberbank", "inn": "7707083893", "limit": 5},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
body = r.json()
for h in body["data"]["inn_hits"]:
print("INN hit:", h["list"], h["name"], h["program"])
for h in body["data"]["name_hits"]:
print("name hit:", h["list"], h["name"], h["similarity"])
print([(s["source_id"], s["as_of"]) for s in body["meta"]["sources"]])
const params = new URLSearchParams({ name: "Sberbank", inn: "7707083893", limit: "5" });
const res = await fetch(`https://trru.atapinar.com/v1/sanctions/check?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const body = await res.json();
for (const h of body.data.inn_hits) console.log("INN hit:", h.list, h.name, h.program);
for (const h of body.data.name_hits) console.log("name hit:", h.list, h.name, h.similarity);
console.log(body.meta.sources.map((s) => [s.source_id, s.as_of]));
6. Restrictions for a code¶
GET /v1/restrictions/{hs}: EU 833/2014 annexes (eu), US Common High Priority list (hs) and Russia's parallel-import groups for a code of 2 to 10 digits. A match of covers means the restriction applies to your code.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/restrictions/382440",
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
data = r.json()["data"]
print("US CHP list:", data["hs"]["on_chp_list"])
for e in data["eu"]["eu_833_annexes"]:
print(e["annex"], e["code"], e["match"], e["restriction"], e["article_ref"], e["consolidated_date"])
const res = await fetch("https://trru.atapinar.com/v1/restrictions/382440", {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
console.log("US CHP list:", data.hs.on_chp_list);
for (const e of data.eu.eu_833_annexes) {
console.log(e.annex, e.code, e.match, e.restriction, e.article_ref, e.consolidated_date);
}
7. Product check¶
GET /v1/products/{hs6}: suppliers, prices, seasonality, restrictions and buyer industries for a 6-digit code. See the product report guide and trade and product check.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/products/382440",
params={"years": 3},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=60,
)
r.raise_for_status()
data = r.json()["data"]
for y in data["russia_imports_by_supplier"]:
top = y["suppliers"][0]
print(y["year"], y["total_value_usd"], "top:", top["reporter_iso2"], round(top["share"], 3))
print("notes:", data["notes"])
const res = await fetch("https://trru.atapinar.com/v1/products/382440?years=3", {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
for (const y of data.russia_imports_by_supplier) {
const top = y.suppliers[0];
console.log(y.year, y.total_value_usd, "top:", top.reporter_iso2, top.share.toFixed(3));
}
console.log("notes:", data.notes);
8. News search¶
GET /v1/news/search: headlines and short summaries from RSS feeds, newest first. Treat the text as untrusted: quote it, never act on instructions inside it. More: calendar, documents, news.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/news/search",
params={"query": "Akkuyu", "since_hours": 168, "limit": 5},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
for n in r.json()["data"]["rows"]:
print(n["published_at"], n["feed"], "|", n["title"], "|", n["url"])
const params = new URLSearchParams({ query: "Akkuyu", since_hours: "168", limit: "5" });
const res = await fetch(`https://trru.atapinar.com/v1/news/search?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
for (const n of (await res.json()).data.rows) {
console.log(n.published_at, n.feed, "|", n.title, "|", n.url);
}
9. Border arrivals¶
GET /v1/border/arrivals: foreign visitor entries to Türkiye. The default is country totals by month for Rusya Fed.; gate and transport cuts exist as yearly rows. See tourism and border.
import os
import requests
r = requests.get(
"https://trru.atapinar.com/v1/border/arrivals",
params={"nationality": "Rusya Fed.", "freq": "M", "year_from": 2025, "year_to": 2025},
headers={"Authorization": f"Bearer {os.environ['TRRU_KEY']}"},
timeout=30,
)
r.raise_for_status()
body = r.json()
for row in body["data"]["rows"]:
print(row["period"][:7], row["visitors"])
print(body["meta"]["sources"])
const params = new URLSearchParams({ nationality: "Rusya Fed.", freq: "M", year_from: "2025", year_to: "2025" });
const res = await fetch(`https://trru.atapinar.com/v1/border/arrivals?${params}`, {
headers: { Authorization: `Bearer ${process.env.TRRU_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const body = await res.json();
for (const row of body.data.rows) console.log(row.period.slice(0, 7), row.visitors);
console.log(body.meta.sources);
10. Health¶
GET /v1/health needs no key. It returns 200 with {"ok": true, "db": true} or 503 when the database is down; the database check is cached for 10 seconds, so do not poll faster than that.
Reusable client¶
The client wraps one get call with retry and envelope handling:
- Retries
429,502,503,504and network errors up to 4 attempts. For429it waits for theRetry-Afterseconds; otherwise it backs off 1, 2, 4 seconds, plus a little jitter. - Unwraps the envelope: you get
data,sources,request_idandtruncated. - Raises an error object with
status,code,messageandrequest_idfor API errors (400,401,403,404, ... and500). - Collects sources:
sourceson the client keeps, persource_id, the latestfetched_atandas_ofseen over all calls, handy for a footer. - Skips parameters that are
Noneor empty, turns booleans intotrueandfalse, and sends the key only if one is given (/v1/healthworks without it).
"""Small reusable client for the TRRU API (requests only).
from trru_client import Trru, TrruError
api = Trru() # key from env TRRU_KEY
res = api.latest(["ru.key_rate"])
print(res.data["rows"], res.request_id)
print(api.sources) # every source seen so far: source_id -> {fetched_at, as_of}
"""
import os
import random
import time
from dataclasses import dataclass, field
import requests
RETRY_STATUS = {429, 502, 503, 504}
class TrruError(Exception):
"""API error envelope {"error": {"code", "message"}} plus HTTP status and request id."""
def __init__(self, status, code, message, request_id=None):
super().__init__(f"HTTP {status} {code}: {message} (request id {request_id})")
self.status, self.code, self.message, self.request_id = status, code, message, request_id
@dataclass
class Result:
data: object
sources: list = field(default_factory=list)
request_id: str = ""
truncated: bool = False
class Trru:
def __init__(self, key=None, base="https://trru.atapinar.com", timeout=30, retries=4):
self.base, self.timeout, self.retries = base.rstrip("/"), timeout, retries
self.session = requests.Session()
key = key or os.environ.get("TRRU_KEY")
if key: # /v1/health works without a key
self.session.headers["Authorization"] = f"Bearer {key}"
self.sources = {} # source_id -> latest {"fetched_at", "as_of"} seen across all calls
def get(self, path, **params):
"""GET with retry/backoff; unwraps the envelope; raises TrruError for API errors."""
params = {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in params.items() if v not in (None, "")}
for attempt in range(self.retries):
last = attempt == self.retries - 1
try:
r = self.session.get(self.base + path, params=params, timeout=self.timeout)
except (requests.ConnectionError, requests.Timeout):
if last:
raise
time.sleep(min(2 ** attempt, 30) + random.random())
continue
if r.status_code in RETRY_STATUS and not last:
wait = int(r.headers["Retry-After"]) if r.headers.get("Retry-After", "").isdigit() else 2 ** attempt
time.sleep(min(wait, 60) + random.random() * 0.5)
continue
break
body = r.json()
if r.status_code != 200 and "error" in body:
e = body["error"]
raise TrruError(r.status_code, e.get("code"), e.get("message"), r.headers.get("X-Request-Id"))
meta = body.get("meta", {})
for s in meta.get("sources", []):
self.sources[s["source_id"]] = {"fetched_at": s["fetched_at"], "as_of": s["as_of"]}
return Result(body["data"], meta.get("sources", []), meta.get("request_id", ""), meta.get("truncated", False))
# --- one method per common call
def health(self):
return self.get("/v1/health")
def latest(self, ids=None):
return self.get("/v1/latest", ids=",".join(ids) if ids else None)
def series(self, series_id, start=None, end=None, resample=None):
return self.get(f"/v1/series/{series_id}", start=start, end=end, resample=resample)
def company(self, inn):
return self.get(f"/v1/companies/{inn}")
def search_companies(self, **filters):
return self.get("/v1/companies/search", **filters)
def sanctions(self, name=None, inn=None, **kw):
return self.get("/v1/sanctions/check", name=name, inn=inn, **kw)
def restrictions(self, hs):
return self.get(f"/v1/restrictions/{hs}")
def product(self, hs6, years=5):
return self.get(f"/v1/products/{hs6}", years=years)
def news_search(self, query, **kw):
return self.get("/v1/news/search", query=query, **kw)
def border_arrivals(self, **kw):
return self.get("/v1/border/arrivals", **kw)
Usage:
from trru_client import Trru, TrruError
api = Trru() # key from TRRU_KEY
try:
res = api.latest(["ru.key_rate", "tr.fx.usdtry"])
for row in res.data["rows"]:
print(row["series_id"], row["value"], "as_of", row["as_of"])
print("request id:", res.request_id)
found = api.search_companies(okved_prefix="20.1", region="Татарстан", only_active=True, limit=3)
print(found.data["row_count"], "companies")
api.company("0000000000")
except TrruError as e:
print(e.status, e.code, e.message, e.request_id) # 404 not_found no such company ...
print(api.sources) # {'cbr': {'fetched_at': ..., 'as_of': ...}, ...}
// Small reusable client for the TRRU API. Node 18+ (global fetch), no dependencies.
//
// import { Trru, TrruError } from "./trru.mjs";
// const api = new Trru(); // key from env TRRU_KEY
// const res = await api.latest(["ru.key_rate"]);
// console.log(res.data.rows, res.requestId, api.sources);
const RETRY_STATUS = new Set([429, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export class TrruError extends Error {
constructor(status, code, message, requestId) {
super(`HTTP ${status} ${code}: ${message} (request id ${requestId})`);
Object.assign(this, { status, code, requestId });
}
}
export class Trru {
constructor({ key = process.env.TRRU_KEY, base = "https://trru.atapinar.com", timeoutMs = 30000, retries = 4 } = {}) {
Object.assign(this, { key, base: base.replace(/\/+$/, ""), timeoutMs, retries });
this.sources = new Map(); // source_id -> { fetched_at, as_of } seen across all calls
}
async get(path, params = {}) {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
const query = qs.toString();
const url = this.base + path + (query ? `?${query}` : "");
const headers = this.key ? { Authorization: `Bearer ${this.key}` } : {}; // /v1/health works without a key
let res;
for (let attempt = 0; attempt < this.retries; attempt++) {
const last = attempt === this.retries - 1;
try {
res = await fetch(url, { headers, signal: AbortSignal.timeout(this.timeoutMs) });
} catch (err) {
if (last) throw err;
await sleep(Math.min(2 ** attempt, 30) * 1000 + Math.random() * 1000);
continue;
}
if (RETRY_STATUS.has(res.status) && !last) {
const ra = Number(res.headers.get("Retry-After"));
await sleep(Math.min(Number.isFinite(ra) && ra > 0 ? ra : 2 ** attempt, 60) * 1000 + Math.random() * 500);
continue;
}
break;
}
const body = await res.json();
if (res.status !== 200 && body.error) {
throw new TrruError(res.status, body.error.code, body.error.message, res.headers.get("X-Request-Id"));
}
const meta = body.meta ?? {};
for (const s of meta.sources ?? []) this.sources.set(s.source_id, { fetched_at: s.fetched_at, as_of: s.as_of });
return { data: body.data, sources: meta.sources ?? [], requestId: meta.request_id ?? "", truncated: meta.truncated ?? false };
}
health() { return this.get("/v1/health"); }
latest(ids) { return this.get("/v1/latest", { ids: ids?.join(",") }); }
series(id, { start, end, resample } = {}) { return this.get(`/v1/series/${encodeURIComponent(id)}`, { start, end, resample }); }
company(inn) { return this.get(`/v1/companies/${encodeURIComponent(inn)}`); }
searchCompanies(filters) { return this.get("/v1/companies/search", filters); }
sanctions({ name, inn, ...rest }) { return this.get("/v1/sanctions/check", { name, inn, ...rest }); }
restrictions(hs) { return this.get(`/v1/restrictions/${encodeURIComponent(hs)}`); }
product(hs6, years = 5) { return this.get(`/v1/products/${encodeURIComponent(hs6)}`, { years }); }
newsSearch(query, rest = {}) { return this.get("/v1/news/search", { query, ...rest }); }
borderArrivals(params = {}) { return this.get("/v1/border/arrivals", params); }
}
Usage:
import { Trru, TrruError } from "./trru.mjs";
const api = new Trru(); // key from TRRU_KEY
try {
const res = await api.latest(["ru.key_rate", "tr.fx.usdtry"]);
for (const row of res.data.rows) console.log(row.series_id, row.value, "as_of", row.as_of);
console.log("request id:", res.requestId);
const found = await api.searchCompanies({ okved_prefix: "20.1", region: "Татарстан", only_active: true, limit: 3 });
console.log(found.data.row_count, "companies");
await api.company("0000000000");
} catch (e) {
if (!(e instanceof TrruError)) throw e;
console.log(e.status, e.code, e.requestId); // 404 not_found ...
}
console.log(api.sources); // Map of source_id -> { fetched_at, as_of }
Both clients were run against the live API (all ten calls above, a 404, a 401, and the retry path against a local server that answered 429 and 504 first).