<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tasamira</title>
    <description>The latest articles on DEV Community by Tasamira (@tasamira).</description>
    <link>https://dev.to/tasamira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4228883%2F7b501c49-a884-47e0-9f8a-fc0b8288ebc0.png</url>
      <title>DEV Community: Tasamira</title>
      <link>https://dev.to/tasamira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tasamira"/>
    <language>en</language>
    <item>
      <title>Central bank exchange rates via public APIs: ECB, NBP and the National Bank of Kazakhstan, with the gotchas</title>
      <dc:creator>Tasamira</dc:creator>
      <pubDate>Thu, 05 Nov 2026 15:49:54 +0000</pubDate>
      <link>https://dev.to/tasamira/central-bank-exchange-rates-via-public-apis-ecb-nbp-and-the-national-bank-of-kazakhstan-with-the-l7e</link>
      <guid>https://dev.to/tasamira/central-bank-exchange-rates-via-public-apis-ecb-nbp-and-the-national-bank-of-kazakhstan-with-the-l7e</guid>
      <description>&lt;p&gt;If you need official exchange rates, central banks are the primary source, and many of them publish without an API key. They do not publish them the same way, though. This post covers three endpoints we use, checked with &lt;code&gt;curl&lt;/code&gt; on 5 November 2026, and the traps that cost us time while building an open dataset of 12 months of central bank rates.&lt;/p&gt;

&lt;p&gt;The dataset is CC BY 4.0 and is on &lt;a href="https://doi.org/10.5281/zenodo.23933050" rel="noopener noreferrer"&gt;Zenodo&lt;/a&gt; and &lt;a href="https://github.com/tasamira/tasamira-data" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It holds two CSV files (snapshot 3 November 2026): the tenge against the US dollar, euro, rouble and yuan, and the dollar and euro priced in 16 national currencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ECB: SDMX API, one trap in the query
&lt;/h2&gt;

&lt;p&gt;The European Central Bank serves reference rates through its &lt;a href="https://data.ecb.europa.eu/help/api/data" rel="noopener noreferrer"&gt;Data Portal API&lt;/a&gt;. The series key for US dollars per euro is &lt;code&gt;EXR/D.USD.EUR.SP00.A&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://data-api.ecb.europa.eu/service/data/EXR/D.USD.EUR.SP00.A?format=csvdata&amp;amp;startPeriod=2026-11-02&amp;amp;endPeriod=2026-11-03"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;&amp;amp;detail=dataonly&lt;/code&gt; to drop the metadata columns and you get a compact CSV:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;FREQ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CURRENCY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;CURRENCY&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;DENOM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;EXR&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;EXR&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;SUFFIX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;TIME&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;PERIOD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;OBS&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;VALUE&lt;/span&gt;
&lt;span class="k"&gt;EXR&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;USD&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;EUR&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;SP&lt;/span&gt;&lt;span class="mf"&gt;00.&lt;/span&gt;&lt;span class="k"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SP&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2026-11-02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.1216&lt;/span&gt;
&lt;span class="k"&gt;EXR&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;USD&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;EUR&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="k"&gt;SP&lt;/span&gt;&lt;span class="mf"&gt;00.&lt;/span&gt;&lt;span class="k"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SP&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="ld"&gt;2026-11-03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.1179&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gotcha: check for an empty body.&lt;/strong&gt; One of our first requests with &lt;code&gt;detail=dataonly&lt;/code&gt; came back empty, and an identical retry minutes later returned the rows above. A collector should treat an empty 200 as an error and retry, not as "no data".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha: the quote direction.&lt;/strong&gt; The ECB publishes units of foreign currency per 1 euro, so &lt;code&gt;1.1179&lt;/code&gt; above means 1 euro = 1.1179 US dollars. If you want euros per dollar, you invert: 1 / 1.1179. We store the inverted value and say so in a &lt;code&gt;note&lt;/code&gt; column, so nobody reads 0.8945 as an ECB figure.&lt;/p&gt;

&lt;p&gt;Our file has the dollar at 1.1514 per euro on 2025-11-03 and 1.1179 on 2026-11-03, a low of 0.835143 euros per dollar on 2026-01-28 and a high of 0.898473 on 2026-10-29 (&lt;a href="https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html" rel="noopener noreferrer"&gt;ECB reference rates&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. NBP (Poland): clean JSON
&lt;/h2&gt;

&lt;p&gt;The Narodowy Bank Polski API is the easiest of the three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.nbp.pl/api/exchangerates/rates/a/usd/2026-11-03/?format=json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"dolar amerykański"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"rates"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"214/A/NBP/2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"effectiveDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-11-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"mid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;3.9526&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;mid&lt;/code&gt; field is the average rate from table A. Each response carries a bulletin number and an &lt;code&gt;effectiveDate&lt;/code&gt;. Store both, since they let you prove later which publication a number came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha: no rate on weekends and holidays.&lt;/strong&gt; Requesting a date with no publication (we tried Sunday 2026-11-01) returns HTTP 404, not the previous day's rate. Walk back day by day, or request a range and take the last element.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. National Bank of Kazakhstan: RSS-style XML
&lt;/h2&gt;

&lt;p&gt;The National Bank of Kazakhstan offers its official rates as XML, per date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://nationalbank.kz/rss/get_rates.cfm?fdate=03.11.2026"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;&amp;lt;item&amp;gt;&lt;/code&gt; holds &lt;code&gt;title&lt;/code&gt; (the currency code), &lt;code&gt;description&lt;/code&gt; (the rate), &lt;code&gt;quant&lt;/code&gt; (the nominal) and &lt;code&gt;change&lt;/code&gt;. For USD on 3 November 2026, &lt;code&gt;description&lt;/code&gt; was &lt;code&gt;453.64&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://nationalbank.kz/rss/get_rates.cfm?fdate=03.11.2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;title&amp;gt;USD&amp;lt;/title&amp;gt;\s*&amp;lt;description&amp;gt;([\d.]+)&amp;lt;/description&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;  &lt;span class="c1"&gt;# 453.64
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(For production use a real XML parser; the regex keeps the example short.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotchas that apply here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The date format is &lt;code&gt;DD.MM.YYYY&lt;/code&gt;, not ISO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rate is for a calendar day, including weekends.&lt;/strong&gt; Unlike the ECB or NBP, the series has a value every day (we got 453.87 for Sunday 2026-11-01), so you do not need the walk-back logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;quant&lt;/code&gt; field matters.&lt;/strong&gt; Some currencies are quoted per 10, 100 or more units (on 3 November 2026: HUF per 10, KRW per 100, VND per 1000). Divide by &lt;code&gt;quant&lt;/code&gt; or your cross rates will be off by an order of magnitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The feed announces rates ahead.&lt;/strong&gt; The RSS feed of current rates (&lt;code&gt;https://nationalbank.kz/rss/rates_all.xml&lt;/code&gt;) can carry a date that is tomorrow: on the evening of 5 November 2026 every &lt;code&gt;pubDate&lt;/code&gt; in it read &lt;code&gt;06.11.2026&lt;/code&gt;. Do not label a row "latest" without checking its date against today's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our file the tenge went from 530.47 per dollar on 2025-11-03 to 453.64 on 2026-11-03 (a low of 439.37 on 2026-09-30), and from 613.75 to 508.17 per euro over the same period (&lt;a href="https://nationalbank.kz/ru/exchangerates/ezhednevnye-oficialnye-rynochnye-kursy-valyut" rel="noopener noreferrer"&gt;National Bank of Kazakhstan&lt;/a&gt;). Computed from those figures, that is -14.5% against the dollar and -17.2% against the euro. Both are central bank reference rates, not what a bank or an exchange office would give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas that are not about one API
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rates are not comparable across banks.&lt;/strong&gt; Central banks use different methods: an official rate for the date, a weighted bank average, a central rate, an indicative rate. Compare changes over time within one source, not levels between countries. We say this in the dataset README because it is the most likely misuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different units, different dates.&lt;/strong&gt; The Danish central bank publishes kroner per 100 units, and we convert to 1 unit. Dates differ by a day or two between currencies, since each bank skips weekends and holidays on its own calendar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some sources cannot be collected automatically.&lt;/strong&gt; We left out the Philippine peso, because the central bank's website does not allow automated collection, and the Uzbek and Peruvian som and sol, whose central bank sites did not answer automated requests on the day of the snapshot. Dropping them and documenting why was better than patching them from a secondary source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the source URL in every row.&lt;/strong&gt; Each row of our CSV has &lt;code&gt;source_url&lt;/code&gt;, &lt;code&gt;retrieved_at&lt;/code&gt; and &lt;code&gt;central_bank&lt;/code&gt;. Someone who doubts a number can click through, and so can you in six months.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use the data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Dataset: &lt;a href="https://doi.org/10.5281/zenodo.23933050" rel="noopener noreferrer"&gt;doi.org/10.5281/zenodo.23933050&lt;/a&gt;, also on &lt;a href="https://github.com/tasamira/tasamira-data" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. CC BY 4.0, credit "Tasamira".&lt;/li&gt;
&lt;li&gt;The tenge study with all tables (in Russian): &lt;a href="https://www.tasamira.com/kz/issledovaniya/kurs-tenge-za-god/" rel="noopener noreferrer"&gt;Курс тенге за год&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The euro and dollar study (in Spanish): &lt;a href="https://www.tasamira.com/es/estudios/evolucion-euro-dolar-12-meses/" rel="noopener noreferrer"&gt;Evolución del euro y el dólar en 12 meses&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find a rate that does not match its central bank source, open an issue on the GitHub repository and we will re-check it against the source.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tasamira data team&lt;/em&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
