temsor API
← Guides

TCMB exchange rate API: pulling USD/TRY into code without the weekend-gap bugs

The Central Bank publishes one XML file per date, nothing for weekends. How to get a date range in one call, gaps carried forward on purpose, and "what rate applied to this invoice" answered right.

Updated 2026-08-09

TCMB (the Turkish central bank) publishes a daily buying/selling rate for 20-odd currencies against the lira, and that feed is what most invoicing, accounting and reconciliation code in Türkiye is legally required to use. Getting one number for today is easy. Getting a clean range — or the rate that was in force on a specific past date — is where teams end up writing more plumbing than they expected.

What makes the source awkward to consume directly

The catalogue

Leave seriesId empty to see everything tracked, including the TCMB series and unrelated ones (fuel prices, etc.) sharing the same endpoint. Filter for tcmb.* client-side:

curl "https://api.temsor.com/v1/series/history"
{
  "catalog": [
    { "id": "tcmb.usd", "title": "ABD DOLARI / TRY — TCMB döviz satış (1 birim)", "source": "TCMB", "points": 68 },
    { "id": "tcmb.eur", "title": "EURO / TRY — TCMB döviz satış (1 birim)", "source": "TCMB", "points": 68 },
    { "id": "tcmb.gbp", "title": "İNGİLİZ STERLİNİ / TRY — TCMB döviz satış (1 birim)", "source": "TCMB", "points": 68 }
    // … 22 currencies in total, each id "tcmb.<iso-lowercase>"
  ]
}
Honest about depth: this archive started on 2026-04-27 and grows by one observation per business day — 68 points as of this writing, not decades. TCMB itself keeps its own historical bulletins back further (tcmb.gov.tr/kurlar/YYYYMM/DDMMYYYY.xml works for any past date you construct yourself); what this endpoint adds is the range query, the weekend/holiday handling below, and a source URL + content hash recorded against every point going forward.

A date range, gaps included as they really happened

curl "https://api.temsor.com/v1/series/history?seriesId=tcmb.usd&from=2026-07-03&to=2026-07-06"
{
  "series": { "id": "tcmb.usd", "title": "ABD DOLARI / TRY — TCMB döviz satış (1 birim)", "unit": "TRY" },
  "points": [
    { "at": "2026-07-03", "value": 46.7178, "filled": false },
    { "at": "2026-07-06", "value": 46.8204, "filled": false }
  ],
  "stats": { "count": 2, "first": 46.7178, "last": 46.8204, "min": 46.7178, "max": 46.8204, "changePct": 0.2196 }
}

2026-07-03 was a Friday, 2026-07-06 a Monday — Saturday and Sunday are simply absent, by default. That is the correct behaviour for a chart of actual bank publications; it is the wrong behaviour if you need one row per calendar day for a report.

Filling the gap: fillGaps=true

curl "https://api.temsor.com/v1/series/history?seriesId=tcmb.usd&from=2026-07-01&to=2026-07-10&fillGaps=true&includeEvidence=true"
{
  "points": [
    { "at": "2026-07-03", "value": 46.7178, "filled": false, "evidence": { "sourceUrl": "https://www.tcmb.gov.tr/kurlar/202607/03072026.xml", "contentHash": "7dbf7113..." } },
    { "at": "2026-07-04", "value": 46.7178, "filled": true,  "evidence": { "sourceUrl": "https://www.tcmb.gov.tr/kurlar/202607/03072026.xml", "contentHash": "7dbf7113..." } },
    { "at": "2026-07-05", "value": 46.7178, "filled": true,  "evidence": { "sourceUrl": "https://www.tcmb.gov.tr/kurlar/202607/03072026.xml", "contentHash": "7dbf7113..." } },
    { "at": "2026-07-06", "value": 46.8204, "filled": false, "evidence": { "sourceUrl": "https://www.tcmb.gov.tr/kurlar/202607/06072026.xml", "contentHash": "a08344c7..." } }
  ]
}

Saturday and Sunday now appear, each carrying Friday's value and filled: true — nothing is interpolated, the last real observation is simply repeated forward. The evidence block still points at Friday's actual bulletin, so a filled row is traceable back to the real publication it came from, not treated as a fresh reading.

The gotcha: fillGaps only fills between two real observations that both fall inside your from/to window. Ask for a single weekend day (from=to=2026-07-04) with fillGaps=true and you get zero points back — there is no later real observation inside the window to fill forward from. Pad the window a few days past your target date and pick the last point at or before it; that is the reliable way to ask "what was in force on this date", shown next.

"What rate applied to this invoice" — done correctly

import Client from 'temsor-api';
const api = new Client();

async function rateOnDate(seriesId, date) {
  const to = new Date(date);
  to.setDate(to.getDate() + 4); // pad past the target so a real observation exists to fill from
  const r = await api.seriesHistory({
    seriesId,
    from: date,
    to: to.toISOString().slice(0, 10),
    fillGaps: true,
  });
  const point = r.points.find((p) => p.at === date) ?? r.points[0];
  if (!point) throw new Error('no observation on or before ' + date + ' in the archive');
  return point; // point.filled tells you whether this was carried forward from a prior business day
}

Multiple currencies at once

There is one series per currency (tcmb.usd, tcmb.eur, tcmb.gbp, …), not a combined "all currencies" call — fetch each you need. Every id follows tcmb.<iso-lowercase>, so building the id from a currency code the user typed is straightforward.

Frequently asked

How far back does the TCMB series go?

This archive began on 2026-04-27 and adds one business-day observation as it goes; it is not a decades-deep history yet. TCMB’s own per-date bulletins (tcmb.gov.tr/kurlar/YYYYMM/DDMMYYYY.xml) go back further if you need dates before that.

What rate is this — buying, selling, effective?

The value returned is TCMB’s published selling rate ("döviz satış") for 1 unit of the currency against TRY, exactly as the bank's bulletin states it.

Why is there no row for Saturday or Sunday by default?

TCMB does not publish a rate on days the bank is closed, so there is nothing to return for those dates unless you pass fillGaps=true, which carries the last published value forward and marks each carried row filled: true.

How do I get the rate that was legally in force on a specific date, including weekends?

Query a window that starts at (or before) that date and ends a few days after it with fillGaps=true, then take the point matching your date. Padding past the target date matters — gap-filling only works between two real observations that are both inside the requested window.

Endpoints used here

/v1/series/history

Open the reference, run it in the browser, no key required.