Quickstart¶
Five steps, about five minutes: get a key, make a call, read the envelope, fetch latest values, check a product.
1. Get a key¶
Keys are issued by the operator of the service; there is no sign-up page. Ask the operator for a key and say what it is for (a script, an MCP client, an internal tool).
| Scope | What it allows |
|---|---|
data |
Every read endpoint and MCP tool |
admin |
Everything in data, plus company contact details and the raw read-only SQL tool in MCP |
Most keys are data only. A key is shown once when it is created and is stored hashed, so a lost key cannot be recovered, only replaced. See Authentication.
Put the key in an environment variable and keep it out of source files:
Check that the service is up (no key needed):
{"data":{"ok":true,"db":true},"meta":{"sources":[],"request_id":"a1d21a853e2c48019fdb19db7a411c76","truncated":false}}
2. First call: the key rate¶
import os
import requests
BASE = "https://trru.atapinar.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TRRU_KEY']}"
resp = session.get(f"{BASE}/v1/latest", params={"ids": "ru.key_rate"}, timeout=30)
resp.raise_for_status()
body = resp.json()
row = body["data"]["rows"][0]
print(row["series_id"], row["value"], row["unit"], "as of", row["as_of"])
// Node 18+ (built-in fetch)
const BASE = "https://trru.atapinar.com";
const headers = { Authorization: `Bearer ${process.env.TRRU_KEY}` };
const resp = await fetch(`${BASE}/v1/latest?ids=ru.key_rate`, { headers });
if (!resp.ok) throw new Error(`${resp.status} ${JSON.stringify(await resp.json())}`);
const body = await resp.json();
const row = body.data.rows[0];
console.log(row.series_id, row.value, row.unit, "as of", row.as_of);
A 401 here means the key is missing or wrong; see Errors.
3. Read the envelope¶
Every successful response has the same shape: the payload under data, and provenance under meta. This is a real response to GET /v1/latest?ids=ru.key_rate,tr.exports_to.ru.usd,no.such.series:
{
"data": {
"rows": [
{
"series_id": "ru.key_rate",
"name_tr": "Rusya MB politika faizi (key rate)",
"unit": "%",
"frequency": "daily",
"as_of": "2026-11-13",
"value": 14.0,
"source_id": "cbr"
},
{
"series_id": "tr.exports_to.ru.usd",
"name_tr": "Türkiye'nin Rusya'a ihracatı (TİM konsolide)",
"unit": "USD",
"frequency": "monthly",
"as_of": "2026-10-01",
"value": 457415414.52000004,
"source_id": "tim"
}
],
"row_count": 2,
"truncated": false
},
"meta": {
"sources": [
{
"source_id": "cbr",
"fetched_at": "2026-11-13T17:30:30.332163+00:00",
"as_of": "2026-11-13"
},
{
"source_id": "tim",
"fetched_at": "2026-11-13T09:45:38.503380+00:00",
"as_of": "2026-10-01"
}
],
"request_id": "c0fe592822fd47a58478944330f7cb42",
"truncated": false
}
}
The third id, no.such.series, does not exist and is simply absent from rows.
data.rows[].as_ofis the date of the observation (the key rate in force on 2026-11-13 is 14.0 %).meta.sources[].fetched_atis when the source was last refreshed successfully.meta.sources[].as_ofis the newest data date in this response.meta.request_idis also returned in theX-Request-Idheader. Quote it when you report a problem.truncated: truemeans a row cap cut the result; narrow the request.
Full details: Responses and sources.
4. Second call: several latest values at once¶
/v1/latest takes a comma-separated list of series ids and returns one row per series.
Rows differ in age because series have different frequencies: the key rate is daily (as_of 2026-11-13 in the sample), while tr.exports_to.ru.usd is monthly and carries the first day of the month it describes (2026-10-01, 457 415 414.52 USD).
Warning
An id that does not exist is left out of rows and listed in data.missing; the response is still 200. If a row you expected is absent, check data.missing first, then look the id up with GET /v1/series?search=... (Series ids and units).
For a time series rather than the newest value:
curl -H "Authorization: Bearer $TRRU_KEY" \
"https://trru.atapinar.com/v1/series/ru.key_rate?start=2026-10-01"
5. A product check¶
/v1/products/{hs6} is a one-call dossier on a six-digit HS product: who supplies Russia, prices, seasonality, restrictions and the Russian buyer base.
resp = session.get(f"{BASE}/v1/products/382440", params={"years": 3}, timeout=60)
resp.raise_for_status()
dossier = resp.json()["data"]
for year in dossier["russia_imports_by_supplier"]:
top = year["suppliers"][0]
print(year["year"], f"total {year['total_value_usd']:,.0f} USD;",
f"top supplier {top['reporter_iso2']} {top['share']:.0%}")
Part of the response (other years, the other suppliers and the blocks monthly_suppliers, china_chapter_monthly, turkey_exports_monthly, unit_prices, seasonality, restrictions, buyers and notes are removed here):
{
"hs6": "382440",
"russia_imports_by_supplier": [
{
"year": 2025,
"total_value_usd": 13130741.575000001,
"suppliers": [
{"reporter": 156, "reporter_iso2": "CN", "value_usd": 7275197.0, "share": 0.5540583491378323},
{"reporter": 398, "reporter_iso2": "KZ", "value_usd": 2405362.97, "share": 0.18318561493736502}
],
"others": {"suppliers": 0, "value_usd": 0.0, "share": 0.0}
}
]
}
These are mirror data: the values are what the exporting countries report as exports to Russia (Glossary). hs6 must be exactly six digits; anything else returns 400 bad_request.
Connect an MCP client¶
For Claude Code, one command registers the server (keep TRRU_KEY exported as above):
claude mcp add --transport http trru https://trru.atapinar.com/mcp \
--header "Authorization: Bearer $TRRU_KEY"
Then ask Claude a question such as "What is the Bank of Russia key rate and when was it last changed?" and it will call the latest and get_series tools. Other clients and the tool list are under MCP in the navigation.
Next steps¶
- Authentication: scopes and key handling.
- Errors and Rate limits: what to retry and what not to.
- Series ids and units: find the series you need.
- Coverage and freshness: what is loaded and how current it is.