Skip to content

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:

export TRRU_KEY=trru_YOUR_KEY

Check that the service is up (no key needed):

curl https://trru.atapinar.com/v1/health
{"data":{"ok":true,"db":true},"meta":{"sources":[],"request_id":"a1d21a853e2c48019fdb19db7a411c76","truncated":false}}

2. First call: the key rate

curl -H "Authorization: Bearer $TRRU_KEY" \
  "https://trru.atapinar.com/v1/latest?ids=ru.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_of is the date of the observation (the key rate in force on 2026-11-13 is 14.0 %).
  • meta.sources[].fetched_at is when the source was last refreshed successfully. meta.sources[].as_of is the newest data date in this response.
  • meta.request_id is also returned in the X-Request-Id header. Quote it when you report a problem.
  • truncated: true means 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.

curl -H "Authorization: Bearer $TRRU_KEY" \
  "https://trru.atapinar.com/v1/latest?ids=ru.key_rate,tr.exports_to.ru.usd,tr.fx.usdtry"
resp = session.get(
    f"{BASE}/v1/latest",
    params={"ids": "ru.key_rate,tr.exports_to.ru.usd,tr.fx.usdtry"},
    timeout=30,
)
resp.raise_for_status()
for row in resp.json()["data"]["rows"]:
    print(f"{row['series_id']:<24} {row['value']:>18,.4f} {row['unit']:<12} {row['as_of']}")
const ids = "ru.key_rate,tr.exports_to.ru.usd,tr.fx.usdtry";
const resp = await fetch(`${BASE}/v1/latest?ids=${ids}`, { headers });
const { data } = await resp.json();
for (const r of data.rows) console.log(r.series_id, r.value, r.unit, r.as_of);

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.

curl -H "Authorization: Bearer $TRRU_KEY" \
  "https://trru.atapinar.com/v1/products/382440?years=3"
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%}")
const resp = await fetch(`${BASE}/v1/products/382440?years=3`, { headers });
const { data } = await resp.json();
for (const y of data.russia_imports_by_supplier) {
  const top = y.suppliers[0];
  console.log(y.year, y.total_value_usd, top.reporter_iso2, top.share);
}

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