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.
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
- One file per date, no range query. The bank serves
tcmb.gov.tr/kurlar/YYYYMM/DDMMYYYY.xmlfor a past date andkurlar/today.xmlfor today. There is no "give me every day between X and Y" — you loop and fetch one file per day. - Weekends and public holidays have no file. The bank does not publish a rate on days it is closed. A naive loop over calendar dates gets a 404 for every Saturday, Sunday and holiday, and now your code needs its own calendar just to know which dates to skip.
- Every XML bundles 20+ currencies. You parse out the one you want, from every file, yourself.
- "What rate applied on this date" is not the same question as "was there a rate published on this date". Accounting rules generally say the rate in force is the last one published on or before the transaction date — which for a Saturday invoice means Friday's rate, not an error.
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>"
]
}
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.
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.