Short answer: The TCMB API for exchange rates is not a documented REST service: the Central Bank of the Republic of Türkiye (TCMB) publishes a keyless daily XML bulletin at tcmb.gov.tr/kurlar/today.xml, plus dated files. Time series come from EVDS, a separate web service that needs a free personal key in an HTTP header; in TCMB reports, APİ means open market operations, not software.
Code that reads TCMB rates usually works on its first run and fails later: on a holiday, a yen invoice or a currency with no banknote rate. Everything below was run or read on 6 November 2026.
These are indicative daily rates, not tradable quotes; this guide gives no view on any currency and is not tax or legal advice. EVDS parameters, registration and terms are in TEFAS, KAP and EVDS data APIs.
Is there a TCMB API? The routes that exist
TCMB offers XML files and a keyed time-series service; no page read on 6 November 2026 documents the XML files as an API, although TCMB's own rates-by-date page (opens in a new tab) requests the same dated files.
| Route | Address | Key |
|---|---|---|
| Latest bulletin | www.tcmb.gov.tr/kurlar/today.xml | None |
| Dated bulletin | /kurlar/YYYYMM/DDMMYYYY.xml, such as /kurlar/202611/03112026.xml | None |
| EVDS time series | evds3.tcmb.gov.tr/igmevdsms-dis/ | Personal key in a header |
Searches for tcmb api işlemleri or tcmb api portföyü are about something else: TCMB's 2021 annual report (opens in a new tab) abbreviates open market operations (açık piyasa işlemleri) as APİ, and its February 2026 Inflation Report has a box on the APİ portfolio.
TCMB exchange rate API fields: what today.xml returns
The root element carries the date and bulletin number, and each Currency row carries up to four lira rates per Unit of foreign currency.
Bulletin 2026/208 has 22 rows. Its root reads Tarih="05.11.2026" (day first), Date="11/05/2026" (month first) and Bulten_No="2026/208", and its Last-Modified header says 12:30:01 GMT, which is 15:30 in Istanbul.
| Code | Unit | ForexBuying | ForexSelling | BanknoteBuying | BanknoteSelling |
|---|---|---|---|---|---|
| USD | 1 | 49.9589 | 50.0489 | 49.9239 | 50.1239 |
| JPY | 100 | 31.4957 | 31.7043 | 31.3792 | 31.8248 |
| CNY | 1 | 7.4491 | 7.5465 | Empty | Empty |
| XDR | 1 | 67.8575 | Empty | Empty | Empty |
Cross rates point both ways: CrossRateOther on the euro is USD per EUR (1.1240), CrossRateUSD on the Australian dollar is AUD per USD (1.4254). Both match middle rates, not buying rates (1.42879 for AUD), so never rebuild lira rates from them. Some rows have five decimals, so avoid fixed widths.
TCMB kur API in Python: a client that needs no key
The client parses a bulletin, divides by Unit, walks back over missing days, caches only files that parse and keeps the bulletin number.
Run at 01:12 Istanbul time on 6 November 2026 (Python 3.11, requests 2.31), when today.xml still held the 5 November bulletin:
from datetime import date, datetime, timedelta
from decimal import Decimal
from pathlib import Path
import xml.etree.ElementTree as ET
import requests
BASE = "https://www.tcmb.gov.tr/kurlar"
CACHE = Path("tcmb_cache")
http = requests.Session()
http.headers["User-Agent"] = "Mozilla/5.0"
def parse(xml_bytes):
root = ET.fromstring(xml_bytes) # bytes: older files are ISO-8859-9
rates = {}
for row in root.iter("Currency"):
if not row.findtext("Unit"): # empty legacy rows in 2004-2005 files
continue
code = row.get("CurrencyCode")
if not code or code in rates: # never overwrite a row silently
raise ValueError(f"missing or repeated code: {code}")
unit = Decimal(row.findtext("Unit"))
rates[code] = {
tag: Decimal(text) / unit if (text := row.findtext(tag)) else None
for tag in ("ForexBuying", "ForexSelling",
"BanknoteBuying", "BanknoteSelling")}
if not all(v is None or (v.is_finite() and v > 0)
for v in rates[code].values()):
raise ValueError(f"NaN, zero or negative rate: {code}")
if not rates:
raise ValueError("not a TCMB bulletin")
return {"set_on": datetime.strptime(root.get("Tarih"), "%d.%m.%Y").date(),
"bulletin": root.get("Bulten_No"), "rates": rates}
def bulletin_on(day):
path = CACHE / f"{day:%Y%m%d}.xml"
if path.exists():
return parse(path.read_bytes())
r = http.get(f"{BASE}/{day:%Y%m}/{day:%d%m%Y}.xml", timeout=20)
if r.status_code == 404:
return None # weekend, holiday, half day or not published yet
r.raise_for_status() # a 403 or 503 is not a holiday
found = parse(r.content) # never cache an error page
CACHE.mkdir(exist_ok=True)
path.write_bytes(r.content)
return found
def latest_on_or_before(day, max_back=10):
for back in range(max_back + 1):
found = bulletin_on(day - timedelta(days=back))
if found:
return found
raise LookupError(f"no bulletin in the {max_back} days before {day}")
r = http.get(f"{BASE}/today.xml", timeout=20)
r.raise_for_status()
today = parse(r.content)
print(today["set_on"], today["bulletin"], len(today["rates"]), "rows")
for code in ("USD", "JPY", "CNY"):
row = today["rates"][code]
print(code, row["ForexBuying"], row["BanknoteBuying"])
for asked in (date(2026, 11, 1), date(2026, 5, 31)):
got = latest_on_or_before(asked)
print(asked, "->", got["set_on"], got["bulletin"], got["rates"]["USD"]["ForexBuying"])Output:
2026-11-05 2026/208 22 rows
USD 49.9589 49.9239
JPY 0.314957 0.313792
CNY 7.4491 None
2026-11-01 -> 2026-10-30 2026/204 49.7747
2026-05-31 -> 2026-05-25 2026/97 45.6312Simulated failures raise instead of passing silently: an HTTP 503, an HTML page sent with status 200 (not cached), a repeated or empty CurrencyCode, a NaN, zero or negative rate, and a file with no usable rows; max_back=5 fails for 31 May 2026. Store bulletin with every converted amount so an auditor can find the source file. Fetch once after 15:30, with a retry; Fincode's market data pipelines page covers scheduled jobs in general.
What breaks a TCMB rates client: cases tested
Most failures come from days with no file, units, blank fields and older files.
| Case | What the files show | What to do |
|---|---|---|
| No bulletin | HTTP 404 for Sunday 1 November 2026, half-day 28 October, Republic Day 29 October, and 6 November before its 15:30 release | Treat only 404 as no bulletin |
| Long gaps | Six straight 404s from 26 to 31 May 2026, between bulletins 2026/97 and 2026/98 | Allow six days or more; the sample uses 10 |
| today.xml date | Still held the 5 November bulletin at 00:45 on 6 November; a holiday could not be tested | Read Tarih; never assume |
| Units | JPY has Unit 100. Hypothetical 10,000 yen at 31.4957 is TRY 3,149.57, not TRY 314,957 | Always divide |
| Blank fields | 10 of 22 rows, including CNY, RUB and XDR, have no banknote rates; XDR has no selling rate | Store nulls, never zero |
| Rows change | 23 rows on 2 January 2025, 22 on 2 January 2026: BGN and IRR gone, KZT added | Key on CurrencyCode, not position |
| Old lira | USD 1336300 on 31 December 2004, 1.3383 on 3 January 2005. The January 2000 file has no Bulten_No | Scale pre-2005 values; allow missing fields |
| Old files | Earliest found: 16 April 1996. ISO-8859-9 through 5 August 2016, UTF-8 from 8 August 2016. Files sampled from 1996 to 2005 show Last-Modified 21 March 2014 | Parse bytes; hash files you rely on |
Across each 2026 gap checked, the bulletin number rose by one, so a larger jump means a missed file.
TCMB EVDS API: key handling and series codes
Use EVDS for history, keep its key in an environment variable and send it only as a header; without a real key, only the refusal paths below could be run.
| Series code | Catalog name |
|---|---|
TP.DK.USD.A.YTL, TP.DK.USD.S.YTL | US dollar buying, selling |
TP.DK.USD.A.EF.YTL | US dollar banknote buying |
TP.DK.JPY.A.YTL | 100 Japanese yen buying |
TP.TUKFIY2025.GENEL | CPI general index, 2025=100 |
import os
import requests
EVDS = "https://evds3.tcmb.gov.tr/igmevdsms-dis/"
def evds_series(series, start, end):
key = os.environ.get("EVDS_API_KEY")
if not key:
raise RuntimeError("EVDS_API_KEY is not set")
url = f"{EVDS}series={'-'.join(series)}&startDate={start}&endDate={end}&type=json"
r = requests.get(url, headers={"key": key, "User-Agent": "Mozilla/5.0"},
timeout=30, allow_redirects=False)
if r.status_code == 200 and "json" in r.headers.get("Content-Type", ""):
return r.json()
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:60]}")
try:
evds_series(["TP.DK.USD.A.YTL", "TP.TUKFIY2025.GENEL"], "01-10-2026", "31-10-2026")
except RuntimeError as err:
print(err)Output with no variable set, then with a made-up key:
EVDS_API_KEY is not set
HTTP 401: Invalid API Key- Documents and behavior differ. The EVDS guide says a missing or wrong key returns 403. On 6 November 2026 no header returned 403 with JSON, but a made-up key returned 401 as plain text.
- Old addresses return HTML with status 200. The former
evds2.tcmb.gov.tr/service/evds/path redirects to the EVDS3 home page, so the sample disables redirects and checks the content type. - Copy codes from the catalog. The English Python guide prints garbled codes such as
TP. MIN. USD. A.YTL, and the web service guide's exampleTP.DK.USD.Ais labeled Archive in the catalog.
Which TCMB rate applies: banks, tax and invoices
TCMB rates are reference values that bind nobody, so the right column depends on the contract or tax rule.
- How they are set. TCMB's method page (opens in a new tab) says it averages hourly interbank USD/TRY quotes from 10:00 to 15:00, applies a discount and premium, and uses cross rates for other currencies. Rates appear on its site at 15:30 and in the Resmî Gazete the next day.
- Forex or banknote. Forex columns are for transfers between accounts, banknote columns for cash. In bulletin 2026/208, USD selling is 0.180% above buying for forex and 0.401% for banknotes.
- Bank and card rates. Banks set their own customer rates; TCMB says the parties decide which rate applies.
| Purpose | Text | Rule |
|---|---|---|
| VAT on a foreign-currency price | VAT Law No. 3065, Art. 26 | Current rate at the moment of the chargeable event; the Law names no TCMB column |
| Valuation | Tax Procedure Law No. 213, Art. 280; General Communiqué No. 590 for 2025 (Resmî Gazete, 24 January 2026), Art. 4 | Exchange quote, else a Finance Ministry rate. With no Ministry rate for the valuation day, TCMB rates: banknote buying for cash (else forex buying), forex buying for non-cash currency; banks use their own buying rates |
Hypothetical example. A USD 1,000 invoice converts to TRY 49,958.90 at the 5 November forex buying rate and TRY 50,048.90 at the selling rate, a TRY 90.00 difference from the column alone.
These texts do not say which bulletin date belongs to a document issued before 15:30. Agree a written rule with your accountant.
TÜİK API: inflation data from SDMX or EVDS
The Turkish Statistical Institute (TÜİK, also TurkStat) documents an API: its Data Portal publishes SDMX Web Service Documentation (opens in a new tab) for nsiws.tuik.gov.tr, which needs an access token.
- API key. Generate one on the portal's user page after SMS verification with a Turkish mobile number; users without one may request a key from info@tuik.gov.tr.
- Token. POST the key as
api_keytogiris.tuik.gov.tr/realms/web/protocol/openid-connect/tokenwithgrant_type=passwordandclient_id=nsi-ws-consumer. - Data. Send the token as a Bearer header to
/rest/data/TR,<dataflow>,<version>. The default is SDMX-ML;format=SDMX-CSVandformat=JSON(JSON-stat) are listed.
Without a token, a data query returned HTTP 401 on 6 November 2026, so the CPI dataflow ID could not be confirmed.
For tüik enflasyon api needs, EVDS also carries the index as TP.TUKFIY2025.GENEL. Check a pipeline against a published figure: TÜİK's annual CPI of 29.77% for October 2026. Method and release timing are in Turkish lira inflation rate; latest values are on the Turkey economic data page.
Can you reuse TCMB rates in a product?
TCMB's main site and EVDS have different terms.
The main site disclaimer (opens in a new tab), on the site that hosts the XML files, allows quoting with reference to the source but says commercial use "shall be subject to prior written permission of the CBRT". The EVDS terms are covered in the EVDS guide.
Neither text mentions today.xml or defines commercial use. Read the published files rather than scraping pages, ask TCMB in writing, keep the reply, cite the source and never present an indicative rate as a tradable price. Exchange price licensing is covered in stock market data API. For data rights in a wider build, see Fincode's project and product analysis page.
Frequently asked questions
Does the TCMB exchange rate API need a key?
No. today.xml and the dated XML files opened without a key on 6 November 2026; EVDS needs a personal key in an HTTP header named key.
What time does TCMB publish exchange rates?
TCMB says it sets indicative rates at 15:30 on business days. No rate is set on weekends, public holidays or half days.
Is there a TÜİK API for inflation data?
Yes. TÜİK's Data Portal documents an SDMX REST API that needs an API key and an access token. The CPI is also in EVDS as TP.TUKFIY2025.GENEL.
What is the TCMB API portfolio?
It is the central bank's open market operations portfolio. APİ abbreviates açık piyasa işlemleri in TCMB reports.
Sources
- TCMB: today.xml, bulletin 2026/208, 5 November 2026 (opens in a new tab)
- TCMB: dated bulletin, 3 November 2026 (opens in a new tab)
- TCMB: indicative exchange rates by date (opens in a new tab)
- TCMB: how indicative rates are set (Turkish) (opens in a new tab)
- TCMB: website disclaimer (opens in a new tab)
- CBRT EVDS: Web Service and API Usage guide (opens in a new tab)
- CBRT EVDS: Python guide (English) (opens in a new tab)
- CBRT EVDS: Terms of Use (opens in a new tab)
- TCMB: 2021 annual report, open market operations (APİ) (Turkish) (opens in a new tab)
- TCMB: Inflation Report 2026-I, box on the APİ portfolio (Turkish) (opens in a new tab)
- TÜİK Data Portal: SDMX Web Service Documentation (opens in a new tab)
- Mevzuat: VAT Law No. 3065, Article 26 (Turkish) (opens in a new tab)
- Mevzuat: Tax Procedure Law No. 213, Article 280 (Turkish) (opens in a new tab)
- Resmî Gazete: Tax Procedure Law General Communiqué No. 590, 24 January 2026, no. 33147 (Turkish) (opens in a new tab)
- TCMB: dated bulletin, 31 December 2004 (old lira) (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.