# TCMB API: exchange rates from today.xml and EVDS in Python

URL: https://fincodelabs.com/en/blog/tcmb-exchange-rate-api · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Fintech development · Published: 2026-11-06 · Updated: 2026-11-06

TCMB API guide: today.xml exchange rates with no key, a tested Python client, holiday gaps, unit traps, EVDS series codes and TÜİK's SDMX route.

**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](https://fincodelabs.com/en/blog/tefas-kap-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](https://www.tcmb.gov.tr/kurlar/kurlar_en.html) requests the same dated files.

**Official routes, checked 6 November 2026**

| 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](https://www3.tcmb.gov.tr/yillikrapor/2021/tr/m-0-4.html) 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.

**TCMB bulletin 2026/208, 5 November 2026 (TRY per Unit)**

| 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:

```python
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.6312
```

Simulated 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](https://fincodelabs.com/en/services/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.

**Findings from TCMB's files, read 6 November 2026**

| 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.

**Codes in the EVDS catalog, read 6 November 2026**

| 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 |

```python
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 example `TP.DK.USD.A` is 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](https://www.tcmb.gov.tr/wps/wcm/connect/TR/TCMB+TR/Main+Menu/Temel+Faaliyetler/Doviz+Efektif/Doviz+ve+Efektif+Piyasalari/Gosterge+Niteligindeki+Kurlar) 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.

**What tax texts say, read 6 November 2026**

| 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](https://veriportali.tuik.gov.tr/en/sdmx-web-service-documentation) for `nsiws.tuik.gov.tr`, which needs an access token.

1. **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.
2. **Token.** POST the key as `api_key` to `giris.tuik.gov.tr/realms/web/protocol/openid-connect/token` with `grant_type=password` and `client_id=nsi-ws-consumer`.
3. **Data.** Send the token as a Bearer header to `/rest/data/TR,<dataflow>,<version>`. The default is SDMX-ML; `format=SDMX-CSV` and `format=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](https://fincodelabs.com/en/blog/turkish-lira-inflation-rate); latest values are on the [Turkey economic data](https://fincodelabs.com/en/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](https://www.tcmb.gov.tr/wps/wcm/connect/EN/TCMB+EN/Bottom+Menu/Other/Disclaimer), 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](https://fincodelabs.com/en/blog/tefas-kap-evds-data-apis).

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](https://fincodelabs.com/en/blog/stock-market-data-api). For data rights in a wider build, see Fincode's [project and product analysis](https://fincodelabs.com/en/services/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](https://www.tcmb.gov.tr/kurlar/today.xml)
- [TCMB: dated bulletin, 3 November 2026](https://www.tcmb.gov.tr/kurlar/202611/03112026.xml)
- [TCMB: indicative exchange rates by date](https://www.tcmb.gov.tr/kurlar/kurlar_en.html)
- [TCMB: how indicative rates are set (Turkish)](https://www.tcmb.gov.tr/wps/wcm/connect/TR/TCMB+TR/Main+Menu/Temel+Faaliyetler/Doviz+Efektif/Doviz+ve+Efektif+Piyasalari/Gosterge+Niteligindeki+Kurlar)
- [TCMB: website disclaimer](https://www.tcmb.gov.tr/wps/wcm/connect/EN/TCMB+EN/Bottom+Menu/Other/Disclaimer)
- [CBRT EVDS: Web Service and API Usage guide](https://evds3.tcmb.gov.tr/igmevdsms-dis/documents/showDocument?docId=16)
- [CBRT EVDS: Python guide (English)](https://evds3.tcmb.gov.tr/igmevdsms-dis/documents/showDocument?docId=13)
- [CBRT EVDS: Terms of Use](https://evds3.tcmb.gov.tr/igmevdsms-dis/documents/showDocument?docId=21)
- [TCMB: 2021 annual report, open market operations (APİ) (Turkish)](https://www3.tcmb.gov.tr/yillikrapor/2021/tr/m-0-4.html)
- [TCMB: Inflation Report 2026-I, box on the APİ portfolio (Turkish)](https://www.tcmb.gov.tr/wps/wcm/connect/aae8a29d-4373-4ee1-a092-d01719cca048/enf26_i_tam.pdf?MOD=AJPERES&CACHEID=ROOTWORKSPACE-aae8a29d-4373-4ee1-a092-d01719cca048-pNgXnT)
- [TÜİK Data Portal: SDMX Web Service Documentation](https://veriportali.tuik.gov.tr/en/sdmx-web-service-documentation)
- [Mevzuat: VAT Law No. 3065, Article 26 (Turkish)](https://www.mevzuat.gov.tr/MevzuatMetin/1.5.3065.pdf)
- [Mevzuat: Tax Procedure Law No. 213, Article 280 (Turkish)](https://www.mevzuat.gov.tr/MevzuatMetin/1.4.213.pdf)
- [Resmî Gazete: Tax Procedure Law General Communiqué No. 590, 24 January 2026, no. 33147 (Turkish)](https://www.resmigazete.gov.tr/eskiler/2026/01/20260124-4.htm)
- [TCMB: dated bulletin, 31 December 2004 (old lira)](https://www.tcmb.gov.tr/kurlar/200412/31122004.xml)

> 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](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/tcmb-exchange-rate-api

Cite as: Fincode, "TCMB API: exchange rates from today.xml and EVDS in Python", updated November 6, 2026, https://fincodelabs.com/en/blog/tcmb-exchange-rate-api
