<?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: Exchange Rate API</title>
    <description>The latest articles on DEV Community by Exchange Rate API (@exchange_rateapi_ee936ad).</description>
    <link>https://dev.to/exchange_rateapi_ee936ad</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%2F3956169%2F6211f582-6b4c-491a-a869-0c2dbcad0c53.png</url>
      <title>DEV Community: Exchange Rate API</title>
      <link>https://dev.to/exchange_rateapi_ee936ad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exchange_rateapi_ee936ad"/>
    <language>en</language>
    <item>
      <title>Exchange Rate API Unit Testing: Mocks, Fixtures and Contract Tests</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:29:29 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/exchange-rate-api-unit-testing-mocks-fixtures-and-contract-tests-3hlf</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/exchange-rate-api-unit-testing-mocks-fixtures-and-contract-tests-3hlf</guid>
      <description>&lt;p&gt;Code that depends on live FX is awkward to test. The rate changes every minute, the network may be down in CI, and a test that asserts &lt;code&gt;0.9215&lt;/code&gt; passes today and fails tomorrow. This guide shows how to test exchange rate API code properly: inject a rate provider, use fixtures with the real response shape, mock HTTP in Vitest or Jest with msw and in Python with responses, cover the 401 and 429 paths, and run one contract test against the real API every night.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: never assert on live values
&lt;/h2&gt;

&lt;p&gt;A test that calls the real API and checks a number is not a test. It is a coin toss with a delay. Split the work in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; run offline with frozen rates. They check your logic: rounding, cross rates, caching, error handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A contract test&lt;/strong&gt; runs on a schedule against the real API. It checks that the response still has the shape your code expects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Inject a rate provider
&lt;/h2&gt;

&lt;p&gt;The easiest code to test never calls &lt;code&gt;fetch&lt;/code&gt; from business logic. It depends on an interface, and the HTTP client is one implementation of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rates.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;RateProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;resetsAt&lt;/span&gt;&lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AllRatesProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RateProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`https://allratestoday.com/api/v1/rates?source=${source}&amp;amp;target=${target}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer ${this.apiKey}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'unauthorized'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rate_limited'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Monthly-Resets'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'http_error'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// The endpoint returns an array, even for one pair.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bad_response'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FixedRateProvider&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RateProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sb"&gt;`${source}/${target}`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'unknown_pair'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Business logic takes a &lt;code&gt;RateProvider&lt;/code&gt; and never knows which one it got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pricing.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;RateProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'./rates'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;priceInMinorUnits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RateProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its tests need no network and no mocks, just frozen numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// pricing.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'vitest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;FixedRateProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'./rates'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;priceInMinorUnits&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'./pricing'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FixedRateProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'USD/EUR'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD/JPY'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;151.37&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'priceInMinorUnits'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'converts and rounds'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;priceInMinorUnits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'leaves USD alone'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;priceInMinorUnits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1999&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fixtures with the real response shape
&lt;/h2&gt;

&lt;p&gt;The HTTP client itself still needs tests, and those need fixtures that match the API exactly. The most common bug in exchange rate code is reading &lt;code&gt;res.rate&lt;/code&gt; when the response is an array. A fixture with the real shape catches it.&lt;br&gt;
&lt;/p&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"source"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-04-03T12:00:00Z"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep one fixture per case: a single pair, several targets (&lt;code&gt;target=EUR,GBP,JPY&lt;/code&gt; returns one array with three objects), the 401 body and a 429. The 401 body is:&lt;br&gt;
&lt;/p&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A valid API key is required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Get a free API key at https://allratestoday.com/register"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mocking fetch with msw in Vitest or Jest
&lt;/h2&gt;

&lt;p&gt;msw intercepts requests at the network layer, so your real client code runs unchanged. This works in Vitest and in Jest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// rates.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;afterEach&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'vitest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;setupServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'msw/node'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'msw'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;AllRatesProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RateError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'./rates'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://allratestoday.com/api/v1/rates'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setupServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'Bearer test-key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'A valid API key is required'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'target'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'2026-04-03T12:00:00Z'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;beforeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;onUnhandledRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resetHandlers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;afterAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AllRatesProvider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reads the rate from the first array element'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AllRatesProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test-key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'throws unauthorized on 401'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AllRatesProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'wrong-key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toMatchObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'unauthorized'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'surfaces the reset time on 429'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'X-Monthly-Limit'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'5000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'X-Monthly-Used'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'5000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                 &lt;span class="s1"&gt;'X-Monthly-Resets'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'2026-12-01T00:00:00Z'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AllRatesProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test-key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeInstanceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RateError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resetsAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-12-01T00:00:00Z'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onUnhandledRequest: 'error'&lt;/code&gt; makes any request you did not mock fail the test, so a stray call to the real API never slips into a unit run. The quota headers only appear on 429 responses; a 200 carries no rate-limit headers, so do not write code that expects them there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python with responses and pytest
&lt;/h2&gt;

&lt;p&gt;The same pattern in Python. The client:&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="c1"&gt;# rates.py&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resets_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rate limited"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resets_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resets_at&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Bearer {key}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RateLimited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Monthly-Resets"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests, with &lt;code&gt;responses&lt;/code&gt; matching both the query string and the header:&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="c1"&gt;# test_rates.py&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matchers&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RateLimited&lt;/span&gt;

&lt;span class="nd"&gt;@responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_reads_first_element&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"source"&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="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-04-03T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;matchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_param_matcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"source"&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="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;matchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header_matcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Bearer test-key"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;get_rate&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="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"test-key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.9215&lt;/span&gt;

&lt;span class="nd"&gt;@responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_401_raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"A valid API key is required"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;get_rate&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="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bad"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_429_exposes_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"X-Monthly-Resets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-12-01T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RateLimited&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;get_rate&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="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"test-key"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resets_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"2026-12-01T00:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For logic above the client, pass a fake in place of &lt;code&gt;get_rate&lt;/code&gt; with &lt;code&gt;monkeypatch&lt;/code&gt;, exactly as with &lt;code&gt;FixedRateProvider&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze time as well as rates
&lt;/h2&gt;

&lt;p&gt;If you cache rates, the cache has a TTL, and TTLs need a clock. Use &lt;code&gt;vi.useFakeTimers()&lt;/code&gt; in Vitest, &lt;code&gt;jest.useFakeTimers()&lt;/code&gt; in Jest or &lt;code&gt;freezegun&lt;/code&gt; in Python, then advance the clock past 60 seconds and assert that the second call goes to the provider. Rates refresh every 60 seconds, so a 60-second cache is a sensible default; &lt;a href="https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/" rel="noopener noreferrer"&gt;the caching guide&lt;/a&gt; covers the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  A nightly contract test against the real API
&lt;/h2&gt;

&lt;p&gt;Mocks only prove your code matches your fixtures. A contract test proves the fixtures still match the API. Keep it small, run it on a schedule, and read the key from CI secrets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/contract/rates.contract.test.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;'vitest'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ALLRATES_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="no"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'AllRatesToday contract'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'returns an array of rate objects'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR,GBP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer ${KEY}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GBP'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeLessThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'rejects a bad key with 401'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Bearer not-a-real-key'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schedule it in GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rates-contract&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'17 3 * * *'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx vitest run tests/contract&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ALLRATES_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ALLRATES_API_KEY }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first test is one request, because several targets cost one request. The 401 test is free, since rejected 401, 403 and 429 requests are not charged. Use a dedicated key for CI so you can rotate it without touching production. The free plan is a one-off allowance meant for evaluation, so a job that runs every night belongs on a paid plan's key; see &lt;a href="https://allratestoday.com/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to assert, and what not to
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assert&lt;/th&gt;
&lt;th&gt;Do not assert&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response is an array; &lt;code&gt;[0].rate&lt;/code&gt; is a positive number&lt;/td&gt;
&lt;td&gt;A specific rate value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;source&lt;/code&gt; and &lt;code&gt;target&lt;/code&gt; echo the request&lt;/td&gt;
&lt;td&gt;That two calls return the same rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;time&lt;/code&gt; parses and is recent&lt;/td&gt;
&lt;td&gt;Rate-limit headers on a 200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;401 on a bad key, 429 handling in mocks&lt;/td&gt;
&lt;td&gt;That the live API is reachable in unit tests&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://allratestoday.com/docs/" rel="noopener noreferrer"&gt;API docs&lt;/a&gt; list every response shape for your fixtures. If you are still picking a provider, the &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt; sets out the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Should unit tests call the real exchange rate API?
&lt;/h3&gt;

&lt;p&gt;No. Unit tests should run offline and give the same result every time. Inject a fixed rate provider or mock the HTTP layer. Call the real API only from a separate contract test that checks the response shape on a schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should a contract test assert if rates change every minute?
&lt;/h3&gt;

&lt;p&gt;The shape, never the value. Check that the response is an array, that rate is a positive number, that source and target echo the request, and that time parses as a recent timestamp.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test the rate limit path?
&lt;/h3&gt;

&lt;p&gt;Mock it. Return a 429 with the X-Monthly-Limit, X-Monthly-Used and X-Monthly-Resets headers and assert that your code backs off or falls back to a cached rate. Those headers appear only on 429 responses, so do not expect them on a 200.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does testing a bad key against the real API use up my quota?
&lt;/h3&gt;

&lt;p&gt;No. Rejected 401, 403 and 429 requests are not charged, so a contract test can check the 401 path with a deliberately wrong key at no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use msw with Jest as well as Vitest?
&lt;/h3&gt;

&lt;p&gt;Yes. msw intercepts requests at the network level in Node, so the same handlers work in Vitest and Jest. Only the import of describe, it and expect differs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/exchange-rate-api-unit-testing-mocks/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Exchange Rates API Data in a Database: Schema, Indexes and As-Of Queries</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:27:42 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/exchange-rates-api-data-in-a-database-schema-indexes-and-as-of-queries-1jd</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/exchange-rates-api-data-in-a-database-schema-indexes-and-as-of-queries-1jd</guid>
      <description>&lt;p&gt;Sooner or later, every app that uses an exchange rates API needs its own copy of the rates. Invoices must be reproducible next year. Reports must use the rate of the transaction date, not today's. And you do not want a page view to depend on a network call. This guide gives a schema that holds up: one table, the right keys, an as-of lookup, idempotent loading and a retention plan. The SQL is for PostgreSQL, with notes for MySQL and SQLite at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;base&lt;/span&gt;        &lt;span class="kt"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- ISO 4217, e.g. 'USD'&lt;/span&gt;
  &lt;span class="n"&gt;quote&lt;/span&gt;       &lt;span class="kt"&gt;CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- 1 base = rate quote&lt;/span&gt;
  &lt;span class="n"&gt;rate&lt;/span&gt;        &lt;span class="kt"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rate_type&lt;/span&gt;   &lt;span class="kt"&gt;TEXT&lt;/span&gt;           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate_type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'official'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;      &lt;span class="kt"&gt;TEXT&lt;/span&gt;           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- 'allratestoday', 'ecb', 'rbi', ...&lt;/span&gt;
  &lt;span class="n"&gt;rate_date&lt;/span&gt;   &lt;span class="kt"&gt;DATE&lt;/span&gt;           &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;-- the date the rate applies to&lt;/span&gt;
  &lt;span class="n"&gt;rate_time&lt;/span&gt;   &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                         &lt;span class="c1"&gt;-- the provider's own timestamp, if any&lt;/span&gt;
  &lt;span class="n"&gt;fetched_at&lt;/span&gt;  &lt;span class="kt"&gt;TIMESTAMPTZ&lt;/span&gt;    &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="kc"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;fx_rate_date_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why each column is there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;base and quote&lt;/strong&gt; fix the direction. A row &lt;code&gt;USD, EUR, 0.9215&lt;/code&gt; means 1 USD = 0.9215 EUR. Write that rule in a comment, because direction bugs are the most expensive kind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rate as &lt;code&gt;NUMERIC(20,10)&lt;/code&gt;&lt;/strong&gt;, never &lt;code&gt;FLOAT&lt;/code&gt; or &lt;code&gt;DOUBLE&lt;/code&gt;. Floating point turns 0.9215 into 0.92149999... and those errors show up as one-cent differences in totals. Ten decimal places covers small-unit currencies against strong ones, and ten integer digits covers rates such as USD/IRR or USD/VND.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rate_type&lt;/strong&gt; separates a live mid-market rate from an official central bank rate. They are different numbers with different purposes, and a report must never mix them by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;source&lt;/strong&gt; names who produced the rate. For official rates it is the institution, such as &lt;code&gt;ecb&lt;/code&gt;. This is the column an auditor asks about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rate_date&lt;/strong&gt; is the business date the rate applies to. &lt;strong&gt;rate_time&lt;/strong&gt; is the provider's timestamp. &lt;strong&gt;fetched_at&lt;/strong&gt; is when your system got it. Keep all three; they answer different questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Primary key and indexes
&lt;/h2&gt;

&lt;p&gt;The primary key &lt;code&gt;(base, quote, rate_type, source, rate_date)&lt;/code&gt; says: one rate per pair, per type, per source, per day. That is the right grain for invoices, accounting and reports.&lt;/p&gt;

&lt;p&gt;The column order is deliberate. Lookups filter on pair, type and source, then range on date, so the primary key index serves the as-of query directly. The extra index on &lt;code&gt;rate_date&lt;/code&gt; is for retention jobs and for "all rates on a date" exports.&lt;/p&gt;

&lt;p&gt;If you also keep intraday snapshots, put them in a separate table keyed on &lt;code&gt;rate_time&lt;/code&gt; instead of &lt;code&gt;rate_date&lt;/code&gt;. Mixing grains in one table makes every query ask "which row of the day?".&lt;/p&gt;

&lt;h2&gt;
  
  
  As-of lookups: the latest rate on or before a date
&lt;/h2&gt;

&lt;p&gt;Rates do not exist for every date. Official rates skip weekends and holidays, and your loader might miss a day. The standard question is "the latest rate on or before date D".&lt;/p&gt;

&lt;p&gt;For several currencies at once, &lt;code&gt;DISTINCT ON&lt;/code&gt; is the idiomatic Postgres answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EUR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'GBP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'JPY'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mid'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'allratestoday'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="kt"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-08'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To attach a rate to each row of another table, use &lt;code&gt;LATERAL&lt;/code&gt;. This converts invoices into USD with the rate on or before each invoice date, and refuses rates more than seven days older than the invoice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
            &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount_usd&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mid'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'allratestoday'&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_date&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoice_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each lateral subquery is a single index probe on the primary key. &lt;code&gt;LEFT JOIN ... ON true&lt;/code&gt; keeps invoices with no rate, so you see a &lt;code&gt;NULL&lt;/code&gt; instead of silently losing rows. Since &lt;code&gt;rate&lt;/code&gt; is EUR per USD, a EUR amount divided by the rate gives USD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not store inverses
&lt;/h2&gt;

&lt;p&gt;It is tempting to store both &lt;code&gt;USD/EUR&lt;/code&gt; and &lt;code&gt;EUR/USD&lt;/code&gt;. Do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They can disagree.&lt;/strong&gt; If one is loaded and the other is not, or both are rounded, &lt;code&gt;a * b&lt;/code&gt; stops being exactly 1, and two reports give two answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doubles the rows&lt;/strong&gt; and the load work for no new information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It hides direction bugs.&lt;/strong&gt; With one direction stored, a query that uses the wrong direction returns nothing, which you notice. With both stored, it returns a plausible wrong number.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store one direction per source and compute the inverse when you need it: &lt;code&gt;1 / rate&lt;/code&gt;. With &lt;code&gt;NUMERIC&lt;/code&gt;, the inverse is exact to the precision you ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triangulation through USD or EUR
&lt;/h2&gt;

&lt;p&gt;You do not need rows for every pair either. With 160 currencies there are over 25,000 pairs. Store each currency against one base and derive the rest. EUR/GBP is USD/GBP divided by USD/EUR:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;eur_gbp&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'USD'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'EUR'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'GBP'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'mid'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'allratestoday'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_date&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important rule: &lt;strong&gt;triangulate only within one source and one date&lt;/strong&gt;. Official rates come in each institution's own base. The ECB quotes against EUR, a central bank in India quotes against INR. Store them as given, with their own base and &lt;code&gt;source = 'ecb'&lt;/code&gt; and so on, and never combine an ECB rate with another bank's rate into a cross rate. The result would belong to no one. The &lt;a href="https://allratestoday.com/blog/how-to-handle-base-currency-conversion/" rel="noopener noreferrer"&gt;base currency conversion guide&lt;/a&gt; goes deeper on cross rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent upserts with ON CONFLICT
&lt;/h2&gt;

&lt;p&gt;Loaders get re-run: a cron fires twice, a job retries after a timeout, someone backfills a week by hand. The insert must be safe to repeat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'mid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'allratestoday'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;rate_time&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;fetched_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;fx_rate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="no"&gt;EXCLUDED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running it twice leaves one row. The &lt;code&gt;WHERE&lt;/code&gt; clause skips writes when nothing changed, which keeps write volume and table bloat down. For official rates, consider &lt;code&gt;DO NOTHING&lt;/code&gt; plus a log entry when a value differs, so a correction by the institution is visible rather than silently overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Loading daily from the exchange rates API
&lt;/h2&gt;

&lt;p&gt;One request fetches every currency you need. The AllRatesToday rates endpoint takes a comma-separated target list and returns a JSON array; a multi-target call costs one request.&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="c1"&gt;# load_rates.py   (pip install requests "psycopg[binary]")&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="no"&gt;TARGETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CAD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CHF"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"INR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CNY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="no"&gt;UPSERT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"""
INSERT INTO fx_rate (base, quote, rate, rate_type, source, rate_date, rate_time)
VALUES (%s, %s, %s, 'mid', 'allratestoday', %s, %s)
ON CONFLICT (base, quote, rate_type, source, rate_date)
DO UPDATE SET rate = EXCLUDED.rate, rate_time = EXCLUDED.rate_time, fetched_at = now()
WHERE fx_rate.rate IS DISTINCT FROM EXCLUDED.rate
"""&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"source"&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="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TARGETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Bearer {os.environ['ALLRATES_API_KEY']}"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_float&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# keep exact decimals&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"+00:00"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UPSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parse_float=Decimal&lt;/code&gt; keeps the rate exact from JSON to &lt;code&gt;NUMERIC&lt;/code&gt;; going through a Python float would reintroduce rounding. Schedule the loader at a fixed time each day, for example just after your business close. If it runs twice the same day, the upsert keeps the later snapshot. Daily loading is about 30 requests a month, which fits any paid plan; commercial use starts on the Small plan, see &lt;a href="https://allratestoday.com/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For backfills, &lt;code&gt;/api/historical-rates?source=USD&amp;amp;target=EUR&amp;amp;period=1y&lt;/code&gt; returns weekly points and costs 12 requests per pair. For daily official rates over a date range, the central bank history endpoint, such as &lt;code&gt;/api/v1/central-bank/ecb/history?source=USD&amp;amp;target=EUR&amp;amp;from=2026-01-01&amp;amp;to=2026-03-31&lt;/code&gt;, returns the institution's own dates and is billed per month of range. See the &lt;a href="https://allratestoday.com/central-bank-rates-api/" rel="noopener noreferrer"&gt;central bank rates API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retention
&lt;/h2&gt;

&lt;p&gt;Daily rows are tiny. 160 currencies for one source is about 58,000 rows a year, a few megabytes with indexes. Keep daily rates forever; old invoices and audits need them.&lt;/p&gt;

&lt;p&gt;Intraday snapshots are different. One snapshot a minute for 160 currencies is about 230,000 rows a day. Partition that table by month and drop partitions you no longer need, keeping only the daily row in &lt;code&gt;fx_rate&lt;/code&gt; long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  MySQL and SQLite notes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL 8&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rate column&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NUMERIC(20,10)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DECIMAL(20,10)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TEXT&lt;/code&gt;, parse in the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upsert&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON CONFLICT ... DO UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON DUPLICATE KEY UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ON CONFLICT ... DO UPDATE&lt;/code&gt; (3.24+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;As-of, many rows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DISTINCT ON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROW_NUMBER() OVER (PARTITION BY quote ORDER BY rate_date DESC)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ROW_NUMBER()&lt;/code&gt; (3.25+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;As-of per row&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LATERAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;LATERAL&lt;/code&gt; (8.0.14+) or correlated subquery&lt;/td&gt;
&lt;td&gt;correlated subquery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timestamps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TIMESTAMPTZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DATETIME&lt;/code&gt; in UTC&lt;/td&gt;
&lt;td&gt;ISO 8601 &lt;code&gt;TEXT&lt;/code&gt; in UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SQLite deserves a warning: a column declared &lt;code&gt;NUMERIC&lt;/code&gt; stores 0.9215 as a floating-point value. Store rates as text and convert with a decimal type in your application.&lt;/p&gt;

&lt;p&gt;If you are still choosing where the data comes from, the &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt; lists update frequencies, history depth and limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What data type should I use for exchange rates in SQL?
&lt;/h3&gt;

&lt;p&gt;Use NUMERIC or DECIMAL, for example NUMERIC(20,10). Never use FLOAT or DOUBLE, because binary floating point cannot represent most decimal rates exactly and the errors show up in totals.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I get the exchange rate for a date with no rate?
&lt;/h3&gt;

&lt;p&gt;Use an as-of lookup: the latest rate on or before the date. In Postgres, use DISTINCT ON for many currencies or a LATERAL subquery with ORDER BY rate_date DESC LIMIT 1 per row, plus a maximum age.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I store both USD/EUR and EUR/USD?
&lt;/h3&gt;

&lt;p&gt;No. Store one direction per source and compute the inverse as 1 divided by the rate. Storing both lets them drift apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I avoid duplicate rows when the loader runs twice?
&lt;/h3&gt;

&lt;p&gt;Make the primary key the natural key (base, quote, rate type, source, rate date) and insert with ON CONFLICT DO UPDATE. Running the load again then updates or skips the row instead of adding one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I mix official and mid-market rates in one table?
&lt;/h3&gt;

&lt;p&gt;Yes, as long as rate_type and source are part of the key and every query filters on them. Never build a cross rate from two different sources.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/store-exchange-rates-database-schema/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Free Currency Converter API for iOS and Android: Swift and Kotlin</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:25:55 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/free-currency-converter-api-for-ios-and-android-swift-and-kotlin-4e64</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/free-currency-converter-api-for-ios-and-android-swift-and-kotlin-4e64</guid>
      <description>&lt;p&gt;A free currency converter API is easy to call from a mobile app. Doing it well takes three more things: the app must work offline with the last known rates, it must tell the user how old those rates are, and the API key must not be inside the app binary. This guide covers all three, with working code for iOS in Swift and Android in Kotlin.&lt;/p&gt;

&lt;p&gt;The examples use the AllRatesToday rates endpoint, which returns mid-market rates for 160+ currencies, refreshed every 60 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture: app, proxy, API
&lt;/h2&gt;

&lt;p&gt;An iOS or Android app is a file the user downloads. Anything compiled into it, including an API key in &lt;code&gt;Info.plist&lt;/code&gt;, &lt;code&gt;BuildConfig&lt;/code&gt; or an obfuscated string, can be extracted with free tools in minutes. Once extracted, the key works from anywhere until you rotate it.&lt;/p&gt;

&lt;p&gt;So the app never talks to the rates API directly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The app calls &lt;code&gt;https://rates.example.com/rates?source=USD&amp;amp;target=EUR,GBP,JPY&lt;/code&gt; on your own backend.&lt;/li&gt;
&lt;li&gt;The backend adds &lt;code&gt;Authorization: Bearer YOUR_API_KEY&lt;/code&gt;, calls AllRatesToday, caches the answer and returns the same JSON array.&lt;/li&gt;
&lt;li&gt;The app decodes the array, saves it with a timestamp and converts amounts locally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The proxy is about 40 lines on Cloudflare Workers or as a Next.js route handler. The full version, with caching, a currency allowlist and rate limiting, is in &lt;a href="https://allratestoday.com/blog/hide-exchange-rate-api-key-proxy/" rel="noopener noreferrer"&gt;how to hide an exchange rate API key behind a proxy&lt;/a&gt;. The rest of this article assumes it exists and returns exactly what the API returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free currency converter API response you decode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR,GBP,JPY"&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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.9215&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-04-03T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.7893&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-04-03T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;151.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-04-03T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a JSON array even when you ask for one pair. Decode it as &lt;code&gt;[Rate]&lt;/code&gt; in Swift and &lt;code&gt;List&amp;lt;Rate&amp;gt;&lt;/code&gt; in Kotlin. Decoding into a single object is the most common error with this endpoint. All targets in one call cost one request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting on the device
&lt;/h2&gt;

&lt;p&gt;Fetch one base, USD, with every currency your app supports. Then any conversion is arithmetic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;USD to EUR: &lt;code&gt;amount * rate[EUR]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;EUR to USD: &lt;code&gt;amount / rate[EUR]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;EUR to JPY: &lt;code&gt;amount / rate[EUR] * rate[JPY]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the app to one request per refresh and lets you cache one response for all users. Round only for display, never before the multiplication. The &lt;a href="https://allratestoday.com/blog/how-to-handle-base-currency-conversion/" rel="noopener noreferrer"&gt;base currency conversion guide&lt;/a&gt; explains cross rates in more detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS: Swift with URLSession, async/await and Codable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Foundation&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Codable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Codable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;

    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatesStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://rates.example.com/rates"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cacheURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cachesDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userDomainMask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendingPathComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rates.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resolvingAgainstBaseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
        &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queryItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;URLQueryItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeoutInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;URLSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;HTTPURLResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;badServerResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nc"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// an array&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;JSONEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cacheURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contentsOf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cacheURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kc"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;time&lt;/code&gt; is decoded as a string so a change in timestamp precision can never break decoding. &lt;code&gt;fetchedAt&lt;/code&gt; is the device's own record of when it last got fresh data; that is the value you show as "rates as of".&lt;/p&gt;

&lt;p&gt;A minimal SwiftUI view model that shows cached rates first and refreshes in the background:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nd"&gt;@MainActor&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConverterModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ObservableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Published&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="nd"&gt;@Published&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isStale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RatesStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TimeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

    &lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeIntervalSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxAge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"INR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;isStale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;isStale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;   &lt;span class="c1"&gt;// keep old rates, flag them&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the view, show &lt;code&gt;Text("Rates as of \(snapshot.fetchedAt.formatted())")&lt;/code&gt;, and a warning when &lt;code&gt;isStale&lt;/code&gt; is true. Call &lt;code&gt;load()&lt;/code&gt; from &lt;code&gt;.task&lt;/code&gt; and again when the scene becomes active.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android: Kotlin with OkHttp and kotlinx.serialization
&lt;/h2&gt;

&lt;p&gt;Add &lt;code&gt;com.squareup.okhttp3:okhttp&lt;/code&gt; and &lt;code&gt;org.jetbrains.kotlinx:kotlinx-serialization-json&lt;/code&gt;, and apply the &lt;code&gt;kotlinx-serialization&lt;/code&gt; Gradle plugin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withContext&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Serializable&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encodeToString&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;kotlinx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Json&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Companion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toHttpUrl&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt;

&lt;span class="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;rateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;firstOrNull&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RatesRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Json&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ignoreUnknownKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;cacheFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rates.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://rates.example.com/rates"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toHttpUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"source"&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HTTP ${res.code}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decodeFromString&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Rate&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// an array&lt;/span&gt;
            &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;cacheFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decodeFromString&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RatesSnapshot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer Ktor, the same code works with &lt;code&gt;HttpClient(OkHttp)&lt;/code&gt; and the &lt;code&gt;ContentNegotiation&lt;/code&gt; plugin: &lt;code&gt;client.get(url).body&amp;lt;List&amp;lt;Rate&amp;gt;&amp;gt;()&lt;/code&gt;. The important part is the same: decode a list.&lt;/p&gt;

&lt;p&gt;A ViewModel that mirrors the Swift version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConverterViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RatesRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;isStale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;maxAgeMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000L&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fetchedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxAgeMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;@&lt;span class="n"&gt;launch&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"INR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CAD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;isStale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;isStale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;refresh()&lt;/code&gt; from &lt;code&gt;onResume&lt;/code&gt;. Show "Rates as of" with &lt;code&gt;DateFormat.getDateTimeInstance().format(Date(snapshot.fetchedAt))&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline behaviour
&lt;/h2&gt;

&lt;p&gt;The cache file is the offline mode. The rules that make it honest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Show cached rates immediately&lt;/strong&gt; on launch, before any network call returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always show the age.&lt;/strong&gt; "Rates as of 11 Nov, 14:02" next to the result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flag old data.&lt;/strong&gt; If the last fetch is more than a day old, show a visible warning, not just a timestamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never invent a rate.&lt;/strong&gt; If there is no cache and no network, say so instead of showing 1.0 or zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Refresh policy
&lt;/h2&gt;

&lt;p&gt;The rates refresh every 60 seconds upstream, but a converter in someone's pocket does not need that. A sensible policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh on app launch and when the app returns to the foreground, if the cache is older than 15 minutes.&lt;/li&gt;
&lt;li&gt;Offer pull-to-refresh, limited to once a minute.&lt;/li&gt;
&lt;li&gt;Do not refresh in the background unless your app has a real need, such as a rate alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the proxy caches the response, upstream usage depends on the proxy's cache time, not on your number of users. With one base and a 10-minute proxy cache, the proxy makes at most 144 calls a day, about 4,320 in 30 days, which fits the Small plan's 5,000 requests a month. One caveat: the Workers Cache API is per data centre, so a global user base can produce a few more upstream calls than that. The guide on &lt;a href="https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/" rel="noopener noreferrer"&gt;caching exchange rates&lt;/a&gt; covers shared caches.&lt;/p&gt;

&lt;p&gt;A free API key is fine for building and testing the app. A published app is commercial use, which starts on the Small plan; see &lt;a href="https://allratestoday.com/pricing/" rel="noopener noreferrer"&gt;pricing&lt;/a&gt;. If you are still comparing providers, the &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt; has the limits side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rates are mid-market
&lt;/h2&gt;

&lt;p&gt;The numbers are mid-market rates: the midpoint between buy and sell prices, with no bank margin. That is the right number for a converter. If your app shows what a bank or card will actually charge, label the result "mid-market rate" so users are not surprised. &lt;a href="https://allratestoday.com/blog/mid-market-vs-retail-exchange-rates/" rel="noopener noreferrer"&gt;Mid-market vs retail rates&lt;/a&gt; explains the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is there a free currency converter API for mobile apps?
&lt;/h3&gt;

&lt;p&gt;Yes. You can get a free API key from AllRatesToday and call the rates endpoint from your own backend for development and testing. A published app counts as commercial use, which starts on the Small plan at 4.99 euros a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I put the API key in my iOS or Android app?
&lt;/h3&gt;

&lt;p&gt;No. Anything in the app binary can be extracted, including obfuscated strings. Keep the key on a small backend proxy and have the app call the proxy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does decoding fail with a type mismatch?
&lt;/h3&gt;

&lt;p&gt;The rates endpoint always returns a JSON array, even for one pair. Decode [Rate] in Swift or List in Kotlin, not a single object.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should the app work offline?
&lt;/h3&gt;

&lt;p&gt;Save the last successful response together with the time you fetched it. On launch, show the cached rates and a "rates as of" time, then refresh when the network is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should a mobile converter refresh rates?
&lt;/h3&gt;

&lt;p&gt;On launch and on return to the foreground when the cache is older than about 15 minutes, plus pull-to-refresh. Upstream rates refresh every 60 seconds, but a converter rarely needs more.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/currency-api-swift-kotlin-mobile/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>kotlin</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Currency Exchange Rates API for Invoices: Which Rate to Use</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:24:08 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/currency-exchange-rates-api-for-invoices-which-rate-to-use-3n00</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/currency-exchange-rates-api-for-invoices-which-rate-to-use-3n00</guid>
      <description>&lt;p&gt;When you issue an invoice in a foreign currency, two questions follow. Which rate converts the amounts into your reporting or tax currency? And which date does that rate come from? A currency exchange rates API for invoices has to answer both, and the answer is usually set by tax law, not by your preference. This guide covers the tax-point rule, the official rates that laws name, what to store on the invoice, and code to fetch an official rate for a given date.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not tax advice.&lt;/strong&gt; This is a developer's summary of common rules. Confirm the rule for your business and country with a tax adviser or the tax authority.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The date: tax point or date of supply
&lt;/h2&gt;

&lt;p&gt;Most VAT and GST systems tie the conversion to one date: the tax point, also called the time or date of supply. That is the date the tax becomes due. Depending on the country and the transaction, it can be the delivery date, the invoice date or the payment date.&lt;/p&gt;

&lt;p&gt;So the rule for your code is simple. Do not use "today's rate" when the invoice is generated. Use the rate for the tax-point date. If you generate an invoice on 12 November for a supply on 9 November, the rate date should follow 9 November, under whatever rule applies to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Official rates named by law
&lt;/h2&gt;

&lt;p&gt;Many tax systems name the source of the rate as well as the date. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UAE VAT.&lt;/strong&gt; Article 69 of the VAT Decree-Law says amounts in a foreign currency are converted at the rate approved by the Central Bank of the UAE at the date of supply. The details, including weekends, are in our &lt;a href="https://allratestoday.com/blog/uae-vat-invoice-exchange-rate-article-69/" rel="noopener noreferrer"&gt;UAE VAT Article 69 guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UK customs and VAT.&lt;/strong&gt; HMRC sets official monthly exchange rates, released before the start of each month. They are the rates for customs declarations and an accepted option for VAT on imports and invoices. See the &lt;a href="https://allratestoday.com/blog/hmrc-exchange-rate-api/" rel="noopener noreferrer"&gt;HMRC exchange rate API&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EU VAT.&lt;/strong&gt; Many EU countries allow the ECB euro reference rate for converting invoice amounts. Some also allow the national central bank's rate. Check your member state's rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;India GST.&lt;/strong&gt; Rule 34 of the CGST Rules points to the CBIC customs rates for goods and to accounting-standard rates for services. See the &lt;a href="https://allratestoday.com/blog/gst-invoice-exchange-rate-india/" rel="noopener noreferrer"&gt;GST invoice exchange rate guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is the same across these: an institution, a date rule, and a table. Your system should fetch from that institution's table, not from a payment provider or a card network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mid-market for display, official for the tax line
&lt;/h2&gt;

&lt;p&gt;An invoice system often needs two different rates, used for two different jobs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Showing an estimated price in a quote or checkout&lt;/td&gt;
&lt;td&gt;Mid-market, current&lt;/td&gt;
&lt;td&gt;Closest to what the market is doing right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Converting the VAT or GST amount on the invoice&lt;/td&gt;
&lt;td&gt;The official rate the rule names&lt;/td&gt;
&lt;td&gt;The tax authority checks against that table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking the invoice in the ledger&lt;/td&gt;
&lt;td&gt;The rate your accounting policy names&lt;/td&gt;
&lt;td&gt;Often the same official table, for consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recording the payment received&lt;/td&gt;
&lt;td&gt;The rate actually applied by the bank&lt;/td&gt;
&lt;td&gt;Gives the realised FX gain or loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mixing these up is the most common mistake. A live mid-market rate is the right number to show a customer a price. It is not the rate a VAT rule names. More on the difference in &lt;a href="https://allratestoday.com/blog/mid-market-vs-retail-exchange-rates/" rel="noopener noreferrer"&gt;mid-market vs retail exchange rates&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to store on the invoice
&lt;/h2&gt;

&lt;p&gt;Store enough to reproduce the number later without calling any API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate.&lt;/strong&gt; At the full precision of the official table. Round the converted amount once, not the rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source.&lt;/strong&gt; The institution, such as "Central Bank of the UAE" or "HMRC".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate date.&lt;/strong&gt; The official date of the table you used. This may differ from the tax-point date on a weekend or holiday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tax-point date.&lt;/strong&gt; The date the rule refers to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lookup time.&lt;/strong&gt; When your system fetched the rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Converted amounts.&lt;/strong&gt; Net, tax and gross in the tax currency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Several systems also require the rate to be printed on the invoice itself. UAE tax invoices are one example. Even where it is not required, printing "Rate: 4.112542 AED per EUR, Central Bank of the UAE, 9 November 2026" answers most questions before they are asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching an official rate for a date
&lt;/h2&gt;

&lt;p&gt;The AllRatesToday central bank endpoint returns an institution's official table for a date: &lt;code&gt;GET /api/v1/central-bank/{bank}/{YYYY-MM-DD}&lt;/code&gt;. The response carries the requested date, the institution's own &lt;code&gt;rate_date&lt;/code&gt;, and the rates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/cbuae/2026-11-08"&lt;/span&gt;
&lt;span class="c"&gt;# "requested_date": "2026-11-08", "rate_date": "2026-11-06",&lt;/span&gt;
&lt;span class="c"&gt;# "published_on_requested_date": false,&lt;/span&gt;
&lt;span class="c"&gt;# "rates": [ ... { "base": "EUR", "quote": "AED", "type": "reference", "value": 4.091466 }, ... ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;8 November 2026 is a Sunday. The CBUAE does not issue rates at weekends, so the response returns Friday's table and says so. Here is the same lookup in JavaScript, with the fields you need to store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;officialRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sb"&gt;`https://allratestoday.com/api/v1/central-bank/${bank}/${date}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer ${process.env.ALLRATES_API_KEY}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Official rate lookup failed: ${res.status}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${bank} has no ${base}/${quote} on ${table.rate_date}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rateDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;taxPointDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;publishedOnDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_on_requested_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// EUR invoice, VAT payable in AED, supply on 8 November 2026&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;officialRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cbuae"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AED"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;netEur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;vatEur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netEur&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;vatAed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vatEur&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// round once, at the end&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`VAT AED ${vatAed.toFixed(2)} at ${fx.rate} (${fx.source}, ${fx.rateDate})`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few points about this code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direction.&lt;/strong&gt; Each row has a &lt;code&gt;base&lt;/code&gt; and a &lt;code&gt;quote&lt;/code&gt;. Check you have the direction you need. If a table only quotes &lt;code&gt;EUR/AED&lt;/code&gt;, then &lt;code&gt;AED/EUR&lt;/code&gt; is &lt;code&gt;1 / value&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekend fallback.&lt;/strong&gt; &lt;code&gt;published_on_requested_date: false&lt;/code&gt; tells you the table came from an earlier day. Store both dates so the fallback is visible in an audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rounding.&lt;/strong&gt; Use the full official precision in the multiplication. Round the tax amount once. For money in production, use integer minor units or a decimal library rather than floats.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache by date.&lt;/strong&gt; A past table does not change. Fetch it once per bank and date, then store it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Swap &lt;code&gt;cbuae&lt;/code&gt; for the institution your rule names: &lt;code&gt;ecb&lt;/code&gt;, &lt;code&gt;hmrc&lt;/code&gt; and the others are listed on the &lt;a href="https://allratestoday.com/central-bank-rates-api/" rel="noopener noreferrer"&gt;central bank rates API page&lt;/a&gt;. The &lt;code&gt;@allratestoday/central-bank&lt;/code&gt; package on npm and &lt;code&gt;allratestoday-central-bank&lt;/code&gt; on PyPI wrap the same endpoint. Dated tables and history are on paid plans, and the Official plan covers any past date with JSON, CSV, XML and XLSX output for ERP imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for your invoicing code
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The rate date follows the tax-point date, not the generation date.&lt;/li&gt;
&lt;li&gt;The source is the institution your tax rule names.&lt;/li&gt;
&lt;li&gt;Weekend and holiday fallback is written down and applied the same way every time.&lt;/li&gt;
&lt;li&gt;Rate, source, rate date and lookup time are stored with the invoice.&lt;/li&gt;
&lt;li&gt;Rounding happens once, on the converted amount.&lt;/li&gt;
&lt;li&gt;Display prices may use a live mid-market rate. The tax line does not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are still choosing a provider for the display side, the &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt; covers update frequency and limits. For the tax side, the question is simpler: which institution does the rule name, and can you get its table for a date.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which exchange rate should I use on a foreign-currency invoice?
&lt;/h3&gt;

&lt;p&gt;For the tax amount, use the rate your tax rule names, taken on the tax point or date of supply. Many countries name an official rate, such as the Central Bank of the UAE rate for UAE VAT or HMRC's monthly rates for UK customs. For showing a price before the sale, a mid-market rate is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a mid-market rate for VAT on an invoice?
&lt;/h3&gt;

&lt;p&gt;Only if your tax rule allows it. Where the law names an official rate, use that rate for the tax line. A mid-market rate is a market snapshot, not the rate a tax authority names.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should I store with each invoice?
&lt;/h3&gt;

&lt;p&gt;Store the rate, the source institution, the official rate date of the table, the date you looked it up, and the converted amounts. That lets you reproduce the figure in an audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if the date of supply is a weekend or a holiday?
&lt;/h3&gt;

&lt;p&gt;Most central banks do not issue rates on non-working days. The usual practice is to use the latest table before the date. Write that fallback into your policy and apply it the same way every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this tax advice?
&lt;/h3&gt;

&lt;p&gt;No. It is a developer's summary of common rules. Confirm the rule that applies to your business with your tax adviser or the tax authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/uae-vat-invoice-exchange-rate-article-69/" rel="noopener noreferrer"&gt;UAE VAT invoice exchange rate: Article 69&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/gst-invoice-exchange-rate-india/" rel="noopener noreferrer"&gt;GST invoice exchange rate in India&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/hmrc-exchange-rate-api/" rel="noopener noreferrer"&gt;HMRC exchange rate API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/ecb-exchange-rate-api/" rel="noopener noreferrer"&gt;ECB exchange rate API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/central-bank-rates-api/" rel="noopener noreferrer"&gt;Central bank exchange rates API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/mid-market-vs-retail-exchange-rates/" rel="noopener noreferrer"&gt;Mid-market vs retail exchange rates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/exchange-rate-api-invoicing-which-rate/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>accounting</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Exchange Rate API Free for Commercial Use? What Free-Tier Terms Say</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:22:21 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/exchange-rate-api-free-for-commercial-use-what-free-tier-terms-say-o4e</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/exchange-rate-api-free-for-commercial-use-what-free-tier-terms-say-o4e</guid>
      <description>&lt;p&gt;Searching for "exchange rate api free" returns plenty of options. The harder question comes later: can you put that free API in a product that makes money? Many free tiers say no, some say yes, and a few say yes with conditions. This guide walks through what free-tier terms typically say, what "commercial" usually covers, and how to check a provider before you ship.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not legal advice.&lt;/strong&gt; This is a summary of public terms at the time of writing. Terms change. Read the current version on each vendor's site before you rely on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What free exchange rate API terms typically say
&lt;/h2&gt;

&lt;p&gt;Free tiers fall into four groups. The table shows where some popular providers sat at the time of writing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Commercial use on free tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixer&lt;/td&gt;
&lt;td&gt;100 requests/month, hourly&lt;/td&gt;
&lt;td&gt;No, non-commercial only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currencylayer&lt;/td&gt;
&lt;td&gt;100 requests/month, daily&lt;/td&gt;
&lt;td&gt;No, non-commercial only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate-API&lt;/td&gt;
&lt;td&gt;1,500 requests/month, daily&lt;/td&gt;
&lt;td&gt;Allowed, but rates are "only suitable for informational or illustrative purposes"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurter&lt;/td&gt;
&lt;td&gt;Free, open source, no key, no quotas&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AllRatesToday&lt;/td&gt;
&lt;td&gt;Free plan, personal use and testing&lt;/td&gt;
&lt;td&gt;No. Commercial use from Small, 4.99 euros/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1. Non-commercial clauses: Fixer and currencylayer
&lt;/h3&gt;

&lt;p&gt;Fixer and currencylayer, both from APILayer, label their free plans as non-commercial. The free plan exists so you can build and evaluate. Once the app is used to run a business, the terms expect a paid plan. Paid plans start at 14.99 dollars a month for both at the time of writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use allowed, fitness limited: ExchangeRate-API
&lt;/h3&gt;

&lt;p&gt;ExchangeRate-API's terms say its rates are "only suitable for informational or illustrative purposes". That is a different kind of limit. It does not ban a commercial site from showing a converted price. It does tell you the vendor does not stand behind the numbers for anything binding, such as invoices, payments or accounting. Its open access endpoint also asks for attribution, covered below.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Commercial use allowed: Frankfurter
&lt;/h3&gt;

&lt;p&gt;Frankfurter is free, open source and allows commercial use. It serves daily data from the ECB and many other central banks. The trade-off is daily data, no support contract and no service level. For many apps that is fine. For a checkout that needs a current rate, it usually is not.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Free for testing, paid for business: AllRatesToday
&lt;/h3&gt;

&lt;p&gt;Our Free plan is for personal use and testing. It includes the latest mid-market rates, refreshed every 60 seconds, and the latest official central bank rates, at 10 requests a minute. It has no historical data, and the free usage is a one-off allowance that does not reset. Commercial use starts on the Small plan at 4.99 euros a month, which also adds history. Full details are on the &lt;a href="https://allratestoday.com/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attribution requirements
&lt;/h2&gt;

&lt;p&gt;Attribution is a separate question from commercial use. A provider can allow commercial use and still require a credit link.&lt;/p&gt;

&lt;p&gt;ExchangeRate-API's open access endpoint says: "We require attribution on the pages you're using these rates with the link below", with the link text "Rates By Exchange Rate API". It notes the link can be styled to fit your page. Its keyed plans have their own terms.&lt;/p&gt;

&lt;p&gt;When you read terms, look for three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where&lt;/strong&gt; the credit must appear: every page that shows a rate, or one page such as an About or Credits page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What&lt;/strong&gt; it must say: exact link text, a logo, or just the provider name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When&lt;/strong&gt; it applies: often only on the free tier or only on public pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a free API requires attribution and your design cannot carry it, that alone is a reason to pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "commercial" usually covers
&lt;/h2&gt;

&lt;p&gt;Most developers think of commercial use as "I charge for it". Terms usually draw the line much earlier. These are the cases that typically count:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SaaS products.&lt;/strong&gt; Any paid app, or a free tier of a paid app, that shows or uses rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ad-supported or affiliate sites.&lt;/strong&gt; A free converter page with ads is earning money from the rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce.&lt;/strong&gt; Showing prices in a local currency to drive sales. See &lt;a href="https://allratestoday.com/blog/exchange-rates-api-ecommerce/" rel="noopener noreferrer"&gt;exchange rates for e-commerce&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal business tools.&lt;/strong&gt; A finance dashboard, a pricing spreadsheet or an ERP sync used inside a company. Nobody outside sees it, but it runs the business.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client work.&lt;/strong&gt; An agency building a site for a paying client.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What usually does not count: a personal budget app you use alone, a university assignment, or a prototype you are testing before launch. Even then, check the wording. Some terms define commercial use as any use "by or on behalf of a business".&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check exchange rate API terms in ten minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open the terms page, not the pricing page.&lt;/strong&gt; Pricing tables simplify. The terms are what apply. Search the page for "commercial", "personal", "attribution", "resale" and "redistribute".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the plan matrix.&lt;/strong&gt; Some vendors mark the free plan as non-commercial only on the pricing page. Treat that as part of the terms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look for a fitness clause.&lt;/strong&gt; Phrases like "informational purposes" or "no warranty of accuracy" tell you not to use the rates for invoices, tax or payments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check redistribution rules.&lt;/strong&gt; If you expose rates through your own API or export them to customers, many terms treat that as resale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check data licences separately.&lt;/strong&gt; An open source server can serve data under a different licence from its code. Read both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save a copy.&lt;/strong&gt; Note the date you read the terms. If the vendor changes them, you know what applied when you built the feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask if unsure.&lt;/strong&gt; A short email to the vendor's support gets a written answer you can keep.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing on a free key, shipping on a paid one
&lt;/h2&gt;

&lt;p&gt;A common pattern is to build and test on a free key, then switch to a paid key before launch. With AllRatesToday the code does not change between plans. Only the key does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR,GBP,JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer ${process.env.ALLRATES_API_KEY}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Rate request failed: ${res.status}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// always an array&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`${r.source}/${r.target} = ${r.rate} at ${r.time}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is an array even for one pair, so read &lt;code&gt;rates[0].rate&lt;/code&gt;, not &lt;code&gt;rates.rate&lt;/code&gt;. Several targets in one call cost one request. Store the key in an environment variable and swap it at deploy time.&lt;/p&gt;

&lt;h2&gt;
  
  
  When free is the right answer
&lt;/h2&gt;

&lt;p&gt;Free is right when the app is personal, when it is a prototype, or when a provider like Frankfurter allows commercial use and daily data is enough. Paid is right when the app earns money and the terms say so, when you need rates more current than once a day, or when you need someone to answer if the data is wrong. For a side-by-side view of limits and update frequency, see our &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt;, and for the cost trade-offs, &lt;a href="https://allratestoday.com/blog/free-vs-paid-exchange-rate-apis/" rel="noopener noreferrer"&gt;free vs paid exchange rate APIs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more point. Commercial use and fitness for purpose are different questions. Even a paid mid-market feed is a market snapshot. If a tax rule or a contract names a rate, use the official table it names. The &lt;a href="https://allratestoday.com/central-bank-rates-api/" rel="noopener noreferrer"&gt;central bank rates API&lt;/a&gt; serves 123 central banks and 6 tax authorities, each with the institution's own rate date.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I use a free exchange rate API in a commercial app?
&lt;/h3&gt;

&lt;p&gt;Sometimes. Fixer and currencylayer limit their free plans to non-commercial use. Frankfurter allows commercial use. ExchangeRate-API allows use but says its rates are only suitable for informational or illustrative purposes. The AllRatesToday Free plan is for personal use and testing; commercial use starts on the Small plan at 4.99 euros a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does an ad-supported website count as commercial use?
&lt;/h3&gt;

&lt;p&gt;Usually yes. Most terms treat any use that earns money, directly or indirectly, as commercial. A blog or tool page with ads or affiliate links generally falls on the commercial side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an internal business tool commercial use?
&lt;/h3&gt;

&lt;p&gt;In most terms, yes. A dashboard or spreadsheet used inside a company to run its business is commercial use even if nobody outside the company sees it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do free exchange rate APIs require attribution?
&lt;/h3&gt;

&lt;p&gt;Some do. ExchangeRate-API's open access endpoint asks for a visible "Rates By Exchange Rate API" link on pages that use the rates. Check the terms of each provider, because attribution rules are usually tied to the plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this legal advice?
&lt;/h3&gt;

&lt;p&gt;No. This article summarises public terms at the time of writing. Read the current terms yourself, and ask a lawyer if the answer matters to your business.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/free-vs-paid-exchange-rate-apis/" rel="noopener noreferrer"&gt;Free vs paid exchange rate APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/exchangerate-api-not-standard-reference-rate/" rel="noopener noreferrer"&gt;Why ExchangeRate-API is not a standard reference rate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/allratestoday-vs-frankfurter-2026/" rel="noopener noreferrer"&gt;AllRatesToday vs Frankfurter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/how-to-choose-exchange-rate-api/" rel="noopener noreferrer"&gt;How to choose an exchange rate API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/pricing/" rel="noopener noreferrer"&gt;AllRatesToday plans and pricing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/free-exchange-rate-api-commercial-use/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>saas</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Live FX Rates API: Polling vs WebSocket, and How to Poll Efficiently</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Wed, 11 Nov 2026 14:20:33 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/live-fx-rates-api-polling-vs-websocket-and-how-to-poll-efficiently-j1e</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/live-fx-rates-api-polling-vs-websocket-and-how-to-poll-efficiently-j1e</guid>
      <description>&lt;p&gt;A &lt;strong&gt;live FX rates API&lt;/strong&gt; sounds like one thing, but "live" covers everything from a tick-by-tick trading stream to a rate that changes once a minute. Which one you need depends on what your app does with the number. This tutorial explains the difference, when polling a REST endpoint is the right design, and how to do it with one API call per minute no matter how many users you have.&lt;/p&gt;

&lt;p&gt;To be clear up front: AllRatesToday is REST only. There is no WebSocket and no push feed. Each rate is a mid-market snapshot refreshed every 60 seconds. The code below shows how to turn that into a live feed for your own clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "live" means for FX rates
&lt;/h2&gt;

&lt;p&gt;There are three common meanings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Update interval&lt;/th&gt;
&lt;th&gt;Contains&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tick stream&lt;/td&gt;
&lt;td&gt;Every price change, many per second for major pairs&lt;/td&gt;
&lt;td&gt;Bid and ask&lt;/td&gt;
&lt;td&gt;WebSocket or HTTP stream from a broker or market data vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snapshot&lt;/td&gt;
&lt;td&gt;Every minute (or hourly, daily)&lt;/td&gt;
&lt;td&gt;One mid-market rate per pair&lt;/td&gt;
&lt;td&gt;REST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixing&lt;/td&gt;
&lt;td&gt;Once a day or month&lt;/td&gt;
&lt;td&gt;One official rate&lt;/td&gt;
&lt;td&gt;REST or a file download&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A tick stream is what a trading screen needs. A snapshot is what a shop, a billing system or a dashboard needs. Nobody re-prices a basket of shoes eight times a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live FX rates API: polling REST vs WebSocket
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt; means your code calls a REST endpoint on a timer. It is stateless, works through every proxy and firewall, is easy to cache, and fails in a simple way: if one call fails, you keep the last value and try again next minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSocket or streaming&lt;/strong&gt; means the server holds a connection open and pushes each change. It has lower latency and no wasted calls. It also needs reconnect logic, heartbeat handling, back-pressure, and a plan for what happens when the stream silently stops.&lt;/p&gt;

&lt;p&gt;Use this rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trading, order entry, tick charts:&lt;/strong&gt; streaming from a broker or market data vendor. You need bid and ask, and latency matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing, checkout, invoices, dashboards, alerts:&lt;/strong&gt; polling a snapshot API. One-minute freshness is more than enough, and the rate is the same for every user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your source updates every 60 seconds, a stream would only push one message a minute anyway. Polling every 60 seconds gets you the same data with less machinery.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to poll efficiently
&lt;/h2&gt;

&lt;p&gt;Most of the cost of polling comes from doing it in the wrong place. Three rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Poll from the server, once.&lt;/strong&gt; Not from each browser. A thousand open tabs should still mean one upstream call a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for every currency in one call.&lt;/strong&gt; &lt;code&gt;target=EUR,GBP,JPY,CAD,AUD&lt;/code&gt; returns one array and costs one request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan out yourself.&lt;/strong&gt; Keep the latest snapshot in memory (or Redis, or KV) and push it to clients over your own Server-Sent Events or WebSocket endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do the arithmetic before you choose an interval. One call per minute is about 1,440 calls a day and about 43,200 in a 30-day month. On AllRatesToday that fits the Large plan (100,000 a month). Every 5 minutes is about 8,640 a month, which fits Medium (10,000). Every 15 minutes is about 2,880, which fits Small (5,000). The &lt;a href="https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/" rel="noopener noreferrer"&gt;caching guide&lt;/a&gt; has more patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node example: poll once, broadcast via SSE
&lt;/h2&gt;

&lt;p&gt;This uses only Node 18+ built-ins: &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;http&lt;/code&gt;. Save it as &lt;code&gt;server.mjs&lt;/code&gt; and run &lt;code&gt;ALLRATES_API_KEY=YOUR_API_KEY node server.mjs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;"node:http"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;ALLRATES_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;TARGETS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CAD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;POLL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;URL_RATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sb"&gt;`https://allratestoday.com/api/v1/rates?source=${BASE}&amp;amp;target=${TARGETS.join(",")}`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// last good snapshot&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// open SSE responses&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`event: rates\ndata: ${JSON.stringify(snapshot)}\n\n`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URL_RATES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer ${API_KEY}`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Monthly limit reached, resets:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"X-Monthly-Resets"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// keep serving the last snapshot&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`HTTP ${res.status}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// an array, one item per target&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Poll failed, keeping last snapshot:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"/rates/stream"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text/event-stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Cache-Control"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"no-cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"keep-alive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"retry: 5000\n\n"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// browser reconnect delay&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// new clients get data at once&lt;/span&gt;
    &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"close"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"/rates"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;               &lt;span class="c1"&gt;// plain JSON for non-SSE clients&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"warming up"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Comment line every 25 s so proxies do not close idle streams.&lt;/span&gt;
&lt;span class="nb"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;": ping\n\n"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;POLL_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000/rates/stream"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One upstream call per interval&lt;/strong&gt;, whatever the number of connected clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads the array correctly.&lt;/strong&gt; &lt;code&gt;/api/v1/rates&lt;/code&gt; always returns an array, so the code maps over &lt;code&gt;rows&lt;/code&gt; rather than reading &lt;code&gt;res.rate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeps the last good snapshot&lt;/strong&gt; on errors and on 429, so clients never see an empty state after the first success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broadcasts only on change&lt;/strong&gt;, so idle periods cost nothing on the client side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carries the rate time.&lt;/strong&gt; &lt;code&gt;time&lt;/code&gt; is when the rate refers to; &lt;code&gt;fetchedAt&lt;/code&gt; is when you got it. Show or log both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The browser side
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;EventSource&lt;/code&gt; is built into every modern browser and reconnects on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"eur"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/rates/stream"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rates"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"eur"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="sb"&gt;`1 USD = ${snap.rates.EUR.toFixed(4)} EUR (as of ${new Date(snap.time).toLocaleTimeString()})`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API key stays on your server. The browser only talks to your endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSE or your own WebSocket?
&lt;/h2&gt;

&lt;p&gt;SSE is one-way, server to client, over plain HTTP. That is exactly the shape of a rate feed. Use a WebSocket only if clients also need to send messages, for example to subscribe to a different set of currencies. In that case, keep the same design: one poller, one shared snapshot, and filter per subscription when you send.&lt;/p&gt;

&lt;p&gt;If you run several server instances, put the snapshot in a shared store and let only one instance poll. A simple lock key with a 60-second expiry in Redis works, or a scheduled job that writes to the store while web instances only read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless: a scheduled job instead of a timer
&lt;/h2&gt;

&lt;p&gt;On serverless platforms there is no long-running process to hold a &lt;code&gt;setInterval&lt;/code&gt;. Split the work in two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A scheduled job&lt;/strong&gt; (a cron trigger, a scheduled function, or a cron entry on a small VM) calls &lt;code&gt;/api/v1/rates&lt;/code&gt; once per interval and writes the snapshot to a shared store such as Redis, a key-value store or a database row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your request handlers&lt;/strong&gt; only read that stored snapshot. They never call the rates API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clients can then poll your own endpoint as often as they like, with a short cache header such as &lt;code&gt;Cache-Control: max-age=30&lt;/code&gt;, or you can keep the SSE endpoint on a small always-on service. Either way the upstream cost stays at one request per interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staleness and failure
&lt;/h2&gt;

&lt;p&gt;Polling makes failure handling simple, but you still need to handle it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mark stale data.&lt;/strong&gt; If &lt;code&gt;fetchedAt&lt;/code&gt; is more than a few intervals old, show a warning or disable checkout in foreign currencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back off on errors.&lt;/strong&gt; On repeated failures, poll less often instead of more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not retry 401.&lt;/strong&gt; A bad key will not fix itself. 401, 403 and 429 responses are not charged, but they will not return data either.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this design is not enough
&lt;/h2&gt;

&lt;p&gt;If you are building a trading screen, polling a one-minute mid-market snapshot is the wrong tool. You need bid and ask prices and a stream from a broker or a market data vendor. The &lt;a href="https://allratestoday.com/blog/best-realtime-forex-api-2026/" rel="noopener noreferrer"&gt;real-time forex API roundup&lt;/a&gt; covers that space, and the &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;free currency API comparison&lt;/a&gt; covers free snapshot APIs.&lt;/p&gt;

&lt;p&gt;For everything else, one REST call a minute and your own SSE endpoint give every user a live rate with very little code.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does AllRatesToday offer a WebSocket feed?
&lt;/h3&gt;

&lt;p&gt;No. It is REST over HTTPS only. Each rate is a one-minute mid-market snapshot, refreshed every 60 seconds. If you need push updates in your app, poll on your server and push to your clients with your own SSE or WebSocket endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I poll a live FX rates API?
&lt;/h3&gt;

&lt;p&gt;No faster than the provider refreshes. If rates update every 60 seconds, polling every 60 seconds gets every update. For most pricing pages every 5 to 15 minutes is enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  When do I actually need WebSocket streaming for FX?
&lt;/h3&gt;

&lt;p&gt;When you trade or show a trading screen: order entry, tick charts, or anything where a price a few seconds old is a problem. For that you need a broker or market data feed with bid and ask prices.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many requests does polling every minute use?
&lt;/h3&gt;

&lt;p&gt;About 1,440 a day, or about 43,200 in a 30-day month, if one call covers all your currencies. Poll every 5 minutes and that drops to about 8,640 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should browsers call the rates API directly?
&lt;/h3&gt;

&lt;p&gt;No. Each visitor would use your quota and could see your key. Poll from your server, cache the result, and serve it to browsers from your own endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live mid-market rates over REST.&lt;/strong&gt; 160+ currencies refreshed every 60 seconds on every plan. One call returns as many targets as you need and counts as one request. &lt;a href="https://allratestoday.com/register" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://allratestoday.com/blog/live-fx-rates-api-polling-vs-websocket/" rel="noopener noreferrer"&gt;allratestoday.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Best Exchange Rate API for Europe in 2026: 8 Compared for EUR Rates, ECB Reference Rates and VAT</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:26:44 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/best-exchange-rate-api-for-europe-in-2026-8-compared-for-eur-rates-ecb-reference-rates-and-vat-762</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/best-exchange-rate-api-for-europe-in-2026-8-compared-for-eur-rates-ecb-reference-rates-and-vat-762</guid>
      <description>&lt;p&gt;For a European business the euro reference rate from the European Central Bank is the default answer to every currency question, and several free APIs serve it. The choice gets harder when you also need a live rate for checkout, a non-euro official rate for a subsidiary in Poland or Switzerland, or output your accounting system can import.&lt;/p&gt;

&lt;p&gt;This comparison looks at eight exchange rate APIs from the point of view of a team in Europe. Free-tier limits and prices were read from each provider's own pricing page on 10 November 2026; they change often, so confirm before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a team in Europe needs from an exchange rate API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live EUR pairs&lt;/strong&gt; for pricing and checkout, not just a daily fixing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECB euro foreign exchange reference rates&lt;/strong&gt;, the rate EU VAT rules allow for invoices in other currencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other European central banks&lt;/strong&gt;: Switzerland, Poland, Czechia, Sweden, Norway, Denmark, Hungary and the UK each publish their own table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History by date&lt;/strong&gt;, for the date VAT became chargeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly averages&lt;/strong&gt;, which Germany uses for VAT returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Updates on free&lt;/th&gt;
&lt;th&gt;ECB reference rates&lt;/th&gt;
&lt;th&gt;Cheapest paid plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AllRatesToday&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free key&lt;/td&gt;
&lt;td&gt;Every 60 seconds&lt;/td&gt;
&lt;td&gt;ECB reference rates, plus the other European central banks and the Swiss customs table&lt;/td&gt;
&lt;td&gt;€4.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurter&lt;/td&gt;
&lt;td&gt;Free, no key, no monthly cap&lt;/td&gt;
&lt;td&gt;Official fixings as each source releases them&lt;/td&gt;
&lt;td&gt;ECB and other European central banks&lt;/td&gt;
&lt;td&gt;No paid plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate-API&lt;/td&gt;
&lt;td&gt;1,500 requests a month&lt;/td&gt;
&lt;td&gt;Once a day&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$10/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Exchange Rates&lt;/td&gt;
&lt;td&gt;1,000 requests a month, USD base&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$12/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurrencyAPI&lt;/td&gt;
&lt;td&gt;300 requests a month, private use only&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$12.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixer&lt;/td&gt;
&lt;td&gt;100 calls a month, non-commercial&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$14.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currencylayer&lt;/td&gt;
&lt;td&gt;100 calls a month, non-commercial&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$14.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XE Currency Data&lt;/td&gt;
&lt;td&gt;Trial only&lt;/td&gt;
&lt;td&gt;Daily on the entry plan&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$599/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The European rates and what they are for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ECB euro reference rates.&lt;/strong&gt; Published for 29 currencies, usually updated at around 16:00 CET every working day, from a concertation at about 14:10 CET. The ECB publishes them for information purposes only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VAT Directive, Article 91(2).&lt;/strong&gt; The exchange rate is the latest selling rate recorded on the most representative exchange market of the Member State when VAT becomes chargeable. Member States shall accept instead the latest exchange rate published by the European Central Bank at that time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Germany.&lt;/strong&gt; For VAT returns, section 16(6) of the German VAT Act requires conversion at the monthly average rates announced by the Federal Ministry of Finance, which are monthly averages of the ECB reference rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outside the euro area.&lt;/strong&gt; Each national central bank publishes its own table, and local rules usually name it. A group with entities in several countries needs each bank's table, not a euro cross rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point for choosing an API: a market mid-rate and an official table are different products. A market rate is right for showing a customer a price. It is not the figure a tax form or an auditor asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight providers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AllRatesToday
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need both kinds of rate. Mid-market rates for 160+ currencies refresh every 60 seconds on every plan, on any base currency, over HTTPS. The same key serves the official tables of 123 central banks and 6 tax authorities, each with the publisher's own date. For Europe that means: ECB reference rates, plus the other European central banks and the Swiss customs table. Output is JSON, CSV, XML or Excel from the same endpoints, with SDKs for JavaScript, Python, PHP, Go and Rust and an MCP server for AI agents. Paid plans start at €4.99 a month; dated tables and history are on paid plans.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Frankfurter
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; projects that only need official fixings and want them free. Frankfurter is open source, needs no key and has no monthly cap. It now aggregates 111 central banks and official sources. For Europe its source list covers: ECB and other European central banks. It serves official fixings, not a live market rate, and it is a free service with no paid tier or support contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. ExchangeRate-API
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; dashboards and reports that need volume and can live with one update a day. The free plan gives 1,500 requests a month with daily updates; the Pro plan is $10 a month for 30,000 requests with hourly updates. Rates are a blended market figure, not a named institution's table.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Open Exchange Rates
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want a long-established vendor and work from a US dollar base. The free plan gives 1,000 requests a month with hourly updates on a USD base; other base currencies start with the $12 a month plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. CurrencyAPI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small private projects. The free plan gives 300 requests a month with daily updates and is for private use only; paid plans start at $12.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Fixer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already on the apilayer marketplace. The free plan gives 100 calls a month with hourly updates for non-commercial use, and base-currency switching is listed only on paid plans, which start at $14.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. currencylayer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the same apilayer stack with a dollar source. The free plan gives 100 calls a month with daily updates for non-commercial use; paid plans start at $14.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. XE Currency Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; banks and large companies that need a recognised vendor on an annual contract. There is a trial but no free plan; the entry plan is $599 a year for 10,000 requests a month with daily rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it for EUR
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# live mid-market rate&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates?source=EUR&amp;amp;target=USD"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt;

&lt;span class="c"&gt;# ECB reference rates, latest table&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/open/central-bank/ecb"&lt;/span&gt;

&lt;span class="c"&gt;# the ECB rate on the date VAT became chargeable&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/ecb/2026-10-15"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call needs a free API key. The official table responses carry the table's own date, so you can show which publication a figure came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you are building&lt;/th&gt;
&lt;th&gt;Look at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A European store or SaaS that needs a rate at checkout&lt;/td&gt;
&lt;td&gt;AllRatesToday for 60-second rates on a EUR base.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VAT invoices on the ECB rate&lt;/td&gt;
&lt;td&gt;Frankfurter is free and keyless and is hard to beat for the ECB fixing alone. AllRatesToday adds CSV, XML and Excel output and live rates on the same key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A group with entities outside the euro area&lt;/td&gt;
&lt;td&gt;An API with each national central bank's table. AllRatesToday and Frankfurter both aggregate them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A dashboard that refreshes once a day&lt;/td&gt;
&lt;td&gt;ExchangeRate-API has the largest free quota for daily data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An enterprise procurement process&lt;/td&gt;
&lt;td&gt;XE, on an annual contract.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best exchange rate API for Europe in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For live EUR rates plus ECB and other European central bank tables on one key, AllRatesToday. For the ECB fixing free and without a key, Frankfurter. For the largest free quota of daily market rates, ExchangeRate-API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the ECB rate for VAT invoices?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Article 91(2) of the EU VAT Directive says Member States shall accept the latest exchange rate published by the European Central Bank at the time the tax becomes chargeable, as an alternative to the latest selling rate on the most representative exchange market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When are ECB reference rates published?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually at around 16:00 CET every working day, based on a concertation between central banks at about 14:10 CET.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a free ECB exchange rate API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Frankfurter serves ECB reference rates free without a key, and the ECB offers its own data portal. AllRatesToday serves the same table with live market rates alongside.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://allratestoday.com/blog/best-exchange-rate-api-for-europe-2026/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt;. Live EUR rates and official central bank tables as JSON, CSV, XML and Excel: &lt;a href="https://allratestoday.com/exchange-rate-api/" rel="noopener noreferrer"&gt;allratestoday.com/exchange-rate-api/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>finance</category>
      <category>europe</category>
    </item>
    <item>
      <title>Best Exchange Rate API for Singapore in 2026: 8 Compared for SGD Rates, MAS Data and GST</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:26:08 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/best-exchange-rate-api-for-singapore-in-2026-8-compared-for-sgd-rates-mas-data-and-gst-16gh</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/best-exchange-rate-api-for-singapore-in-2026-8-compared-for-sgd-rates-mas-data-and-gst-16gh</guid>
      <description>&lt;p&gt;Singapore companies trade in dollars, euros, yen, ringgit and rupiah as a matter of routine, and report GST in Singapore dollars. IRAS is specific about where the exchange rate may come from and how consistently it must be used. An API that serves the Monetary Authority of Singapore table alongside live rates covers both the product and the GST return.&lt;/p&gt;

&lt;p&gt;This comparison looks at eight exchange rate APIs from the point of view of a team in Singapore. Free-tier limits and prices were read from each provider's own pricing page on 10 November 2026; they change often, so confirm before you commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a team in Singapore needs from an exchange rate API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live SGD pairs&lt;/strong&gt; across Asian and major currencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetary Authority of Singapore rates&lt;/strong&gt;, one of the sources IRAS accepts for GST.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fixed, documented source&lt;/strong&gt;: IRAS requires the chosen rate to be used consistently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional coverage&lt;/strong&gt;: Malaysia, Indonesia, Thailand, Hong Kong and China on the same key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Updates on free&lt;/th&gt;
&lt;th&gt;MAS exchange rates&lt;/th&gt;
&lt;th&gt;Cheapest paid plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AllRatesToday&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free key&lt;/td&gt;
&lt;td&gt;Every 60 seconds&lt;/td&gt;
&lt;td&gt;MAS daily rates (21 currencies)&lt;/td&gt;
&lt;td&gt;€4.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurter&lt;/td&gt;
&lt;td&gt;Free, no key, no monthly cap&lt;/td&gt;
&lt;td&gt;Official fixings as each source releases them&lt;/td&gt;
&lt;td&gt;MAS&lt;/td&gt;
&lt;td&gt;No paid plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate-API&lt;/td&gt;
&lt;td&gt;1,500 requests a month&lt;/td&gt;
&lt;td&gt;Once a day&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$10/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Exchange Rates&lt;/td&gt;
&lt;td&gt;1,000 requests a month, USD base&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$12/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurrencyAPI&lt;/td&gt;
&lt;td&gt;300 requests a month, private use only&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$12.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixer&lt;/td&gt;
&lt;td&gt;100 calls a month, non-commercial&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$14.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;currencylayer&lt;/td&gt;
&lt;td&gt;100 calls a month, non-commercial&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$14.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XE Currency Data&lt;/td&gt;
&lt;td&gt;Trial only&lt;/td&gt;
&lt;td&gt;Daily on the entry plan&lt;/td&gt;
&lt;td&gt;No, market rates only&lt;/td&gt;
&lt;td&gt;$599/yr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Singapore rates and what they are for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MAS exchange rates.&lt;/strong&gt; Daily rates for 21 currencies against the Singapore dollar, described by MAS as the average of buying and selling interbank rates quoted around midday in Singapore, with weekly and monthly averages also published.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IRAS acceptable sources for GST.&lt;/strong&gt; Local banks, locally circulated newspapers and MAS are acceptable local sources. Reputable news agencies and foreign central banks without exchange controls are acceptable global sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IRAS consistency rule.&lt;/strong&gt; The exchange rate selected must be updated at least once every three months and used consistently for at least one year from the end of the accounting period in which the source was first used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imports.&lt;/strong&gt; Input tax on imports is claimed on the Singapore dollar amounts shown in the import permits issued by Singapore Customs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point for choosing an API: a market mid-rate and an official table are different products. A market rate is right for showing a customer a price. It is not the figure a tax form or an auditor asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight providers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AllRatesToday
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that need both kinds of rate. Mid-market rates for 160+ currencies refresh every 60 seconds on every plan, on any base currency, over HTTPS. The same key serves the official tables of 123 central banks and 6 tax authorities, each with the publisher's own date. For Singapore that means: MAS daily rates (21 currencies). Output is JSON, CSV, XML or Excel from the same endpoints, with SDKs for JavaScript, Python, PHP, Go and Rust and an MCP server for AI agents. Paid plans start at €4.99 a month; dated tables and history are on paid plans.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Frankfurter
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; projects that only need official fixings and want them free. Frankfurter is open source, needs no key and has no monthly cap. It now aggregates 111 central banks and official sources. For Singapore its source list covers: MAS. It serves official fixings, not a live market rate, and it is a free service with no paid tier or support contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. ExchangeRate-API
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; dashboards and reports that need volume and can live with one update a day. The free plan gives 1,500 requests a month with daily updates; the Pro plan is $10 a month for 30,000 requests with hourly updates. Rates are a blended market figure, not a named institution's table.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Open Exchange Rates
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want a long-established vendor and work from a US dollar base. The free plan gives 1,000 requests a month with hourly updates on a USD base; other base currencies start with the $12 a month plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. CurrencyAPI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small private projects. The free plan gives 300 requests a month with daily updates and is for private use only; paid plans start at $12.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Fixer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams already on the apilayer marketplace. The free plan gives 100 calls a month with hourly updates for non-commercial use, and base-currency switching is listed only on paid plans, which start at $14.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. currencylayer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; the same apilayer stack with a dollar source. The free plan gives 100 calls a month with daily updates for non-commercial use; paid plans start at $14.99 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. XE Currency Data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; banks and large companies that need a recognised vendor on an annual contract. There is a trial but no free plan; the entry plan is $599 a year for 10,000 requests a month with daily rates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it for SGD
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# live mid-market rate&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=SGD"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt;

&lt;span class="c"&gt;# MAS, latest table&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/open/central-bank/mas"&lt;/span&gt;

&lt;span class="c"&gt;# the MAS table for an invoice date&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/mas/2026-09-30"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call needs a free API key. The official table responses carry the table's own date, so you can show which publication a figure came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If you are building&lt;/th&gt;
&lt;th&gt;Look at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A Singapore platform pricing across Southeast Asia&lt;/td&gt;
&lt;td&gt;AllRatesToday for 60-second rates on an SGD base, with the region's central banks on the same key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GST invoices on the MAS rate&lt;/td&gt;
&lt;td&gt;AllRatesToday or Frankfurter. Pick one source, document it, and keep it for at least a year as IRAS requires.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An ERP or accounting rate feed&lt;/td&gt;
&lt;td&gt;AllRatesToday returns the MAS table as CSV, XML and Excel.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A dashboard that refreshes once a day&lt;/td&gt;
&lt;td&gt;ExchangeRate-API has the largest free quota for daily data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An enterprise procurement process&lt;/td&gt;
&lt;td&gt;XE, on an annual contract.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best exchange rate API for Singapore in 2026?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For live SGD rates plus the MAS table on one key, AllRatesToday. For MAS fixings free and without a key, Frankfurter. For the largest free quota of daily market rates, ExchangeRate-API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which exchange rate does IRAS accept for GST?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IRAS accepts rates from local banks, locally circulated newspapers and MAS, and from reputable news agencies and foreign central banks without exchange controls. The source must be updated at least every three months and used consistently for at least a year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many currencies does MAS publish?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;21 currencies against the Singapore dollar, as daily rates with weekly and monthly averages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I change my GST exchange rate source during the year?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IRAS requires the selected rate to be used consistently for at least one year from the end of the accounting period in which the source was first used.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://allratestoday.com/blog/best-exchange-rate-api-for-singapore-2026/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt;. Live SGD rates and official central bank tables as JSON, CSV, XML and Excel: &lt;a href="https://allratestoday.com/exchange-rate-api/" rel="noopener noreferrer"&gt;allratestoday.com/exchange-rate-api/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>finance</category>
      <category>singapore</category>
    </item>
    <item>
      <title>Fed H.10 Exchange Rate History: Download Federal Reserve Daily Rates 2016 to 2025 as CSV, Excel or API</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:03:25 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/fed-h10-exchange-rate-history-download-federal-reserve-daily-rates-2016-to-2025-as-csv-excel-or-543k</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/fed-h10-exchange-rate-history-download-federal-reserve-daily-rates-2016-to-2025-as-csv-excel-or-543k</guid>
      <description>&lt;p&gt;The Federal Reserve's H.10 release is the standard public source for daily US dollar exchange rates: it is what tax preparers use for a transaction-date rate, what analysts use for a clean long series, and what many accounting policies name for translation. The Fed's own download tool works, but it hands you one wide file with its own codes and "ND" markers. This guide shows the series already tidied, and how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What H.10 is
&lt;/h2&gt;

&lt;p&gt;The Federal Reserve describes the rates as "noon buying rates in New York for cable transfers payable in foreign currencies", "certified by the Federal Reserve Bank of New York for customs purposes as required by section 522 of the amended Tariff Act of 1930". The schedule: "On Mondays at 4:15 p.m. the Federal Reserve Board releases daily bilateral exchange rates and U.S. dollar indexes for the previous business week." If Monday is a federal holiday, the release moves to the next business day.&lt;/p&gt;

&lt;p&gt;Three practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data is daily but arrives weekly.&lt;/strong&gt; Monday's release contains the five business days of the week before. The rate for last Tuesday is not available until the following Monday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rate on federal holidays.&lt;/strong&gt; The Fed's files show "ND" (no data). In 2025 the series has 250 daily tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four currencies are quoted the other way round.&lt;/strong&gt; The euro, pound, Australian dollar and New Zealand dollar are in US dollars per unit. Everything else is in currency units per US dollar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tidy series: one CSV per year
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/AllRates-Today/central-bank-exchange-rates/tree/main/data/fed" rel="noopener noreferrer"&gt;central-bank-exchange-rates&lt;/a&gt; repository holds the official H.10 tables under &lt;code&gt;data/fed/history/&lt;/code&gt;, one file per year from 2016:&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="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//raw.githubusercontent.com/AllRates-Today/central-bank-exchange-rates/main/data/fed/history/2025.csv&lt;/span&gt;

&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;AUD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.6208&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;EUR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.0261&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;USD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="no"&gt;JPY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;quote&lt;/code&gt; columns make the direction explicit: &lt;code&gt;EUR,USD,1.0261&lt;/code&gt; is dollars per euro, &lt;code&gt;USD,JPY&lt;/code&gt; is yen per dollar. Holidays are simply absent, with no "ND" rows to filter. The files cover 22 currencies. Licence: &lt;strong&gt;CC BY-NC 4.0&lt;/strong&gt;, free for research, personal filing and internal non-commercial use with attribution; commercial systems use the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Excel and Google Sheets
&lt;/h2&gt;

&lt;p&gt;In Excel: Data &amp;gt; Get Data &amp;gt; From Web, paste the raw URL, and Power Query loads a table you can filter by &lt;code&gt;base&lt;/code&gt; or &lt;code&gt;quote&lt;/code&gt;. In Google Sheets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=QUERY(IMPORTDATA("https://raw.githubusercontent.com/AllRates-Today/central-bank-exchange-rates/main/data/fed/history/2025.csv"),
       "select Col1, Col5 where Col2 = 'EUR'", 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  pandas: one frame, one direction
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://raw.githubusercontent.com/AllRates-Today/central-bank-exchange-rates/main/data/fed/history/{}.csv"&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_dates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2016&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# normalise everything to "foreign currency units per 1 USD"&lt;/span&gt;
&lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"USD"&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ccy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"per_usd"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;wide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ccy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"per_usd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2025-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"CAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# EUR 0.8521, GBP 0.7437, JPY 156.8000, CAD 1.3712&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2025"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"JPY"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# 2025 averages&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2025-12-25"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="c1"&gt;# holiday -&amp;gt; Dec 24 rate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in that snippet are the ones that cause wrong numbers elsewhere. The inversion puts all currencies on one basis before you average or compare. And &lt;code&gt;asof&lt;/code&gt; returns the last available rate for a holiday or weekend date, which is the usual fallback for a transaction dated on a non-business day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check values
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;EUR (USD per EUR)&lt;/th&gt;
&lt;th&gt;GBP (USD per GBP)&lt;/th&gt;
&lt;th&gt;JPY per USD&lt;/th&gt;
&lt;th&gt;CAD per USD&lt;/th&gt;
&lt;th&gt;INR per USD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dec 31, 2024&lt;/td&gt;
&lt;td&gt;1.0351&lt;/td&gt;
&lt;td&gt;1.2521&lt;/td&gt;
&lt;td&gt;157.37&lt;/td&gt;
&lt;td&gt;1.4400&lt;/td&gt;
&lt;td&gt;85.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dec 31, 2025&lt;/td&gt;
&lt;td&gt;1.1736&lt;/td&gt;
&lt;td&gt;1.3447&lt;/td&gt;
&lt;td&gt;156.80&lt;/td&gt;
&lt;td&gt;1.3712&lt;/td&gt;
&lt;td&gt;89.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025 average (250 days)&lt;/td&gt;
&lt;td&gt;1.1306&lt;/td&gt;
&lt;td&gt;1.3192&lt;/td&gt;
&lt;td&gt;149.57&lt;/td&gt;
&lt;td&gt;1.3973&lt;/td&gt;
&lt;td&gt;87.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025 low&lt;/td&gt;
&lt;td&gt;1.0209&lt;/td&gt;
&lt;td&gt;1.2158&lt;/td&gt;
&lt;td&gt;140.81&lt;/td&gt;
&lt;td&gt;1.3556&lt;/td&gt;
&lt;td&gt;84.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025 high&lt;/td&gt;
&lt;td&gt;1.1848&lt;/td&gt;
&lt;td&gt;1.3754&lt;/td&gt;
&lt;td&gt;158.31&lt;/td&gt;
&lt;td&gt;1.4601&lt;/td&gt;
&lt;td&gt;91.03&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  One date, as an API call
&lt;/h2&gt;

&lt;p&gt;The latest released table:&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="s2"&gt;"https://allratestoday.com/api/open/central-bank/fed"&lt;/span&gt;
&lt;span class="c"&gt;# "rate_date": "2026-11-06", "cadence": "weekly",&lt;/span&gt;
&lt;span class="c"&gt;# "rates": [ { "base": "EUR", "quote": "USD", "type": "reference", "value": 1.1154 }, ... ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A past date or a range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/fed/2025-12-31"&lt;/span&gt;

curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/fed/history?symbol=EUR&amp;amp;from=2025-01-01&amp;amp;to=2025-12-31"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;?format=csv&lt;/code&gt;, &lt;code&gt;xml&lt;/code&gt; or &lt;code&gt;xlsx&lt;/code&gt; to any table endpoint. Dated tables and history are on paid plans; a history request counts as one call per currency per month of data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the series is used for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transaction-date rates&lt;/strong&gt; for a sale, dividend or bonus on a US return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment-date rates and yearly averages&lt;/strong&gt; for the foreign tax credit. See &lt;a href="https://allratestoday.com/blog/foreign-tax-credit-form-1116-exchange-rate/" rel="noopener noreferrer"&gt;the Form 1116 guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fiscal-year averages&lt;/strong&gt; for Form 5471. See &lt;a href="https://allratestoday.com/blog/form-5471-exchange-rates-average-year-end/" rel="noopener noreferrer"&gt;Form 5471 exchange rates&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly averages&lt;/strong&gt; for a salary on Form 2555. See &lt;a href="https://allratestoday.com/blog/form-2555-foreign-earned-income-exchange-rate/" rel="noopener noreferrer"&gt;the Form 2555 guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the FBAR and Form 8938, do not use H.10: those forms call for the Treasury year-end rate. The three tables are compared in &lt;a href="https://allratestoday.com/blog/exchange-rates-december-31-2025-treasury-fed-irs/" rel="noopener noreferrer"&gt;exchange rates as of December 31, 2025&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where can I download Federal Reserve H.10 historical exchange rates?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Federal Reserve offers the series through its H.10 pages and data download program. The same official daily tables are available as one tidy CSV per year from 2016 in the AllRates-Today central-bank-exchange-rates repository, and by date through the AllRatesToday API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often is H.10 published?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Weekly. On Mondays at 4:15 p.m. the Federal Reserve Board releases daily rates for the previous business week, or on the next business day if Monday is a federal holiday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which H.10 currencies are quoted in dollars per unit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The euro, the UK pound, the Australian dollar and the New Zealand dollar. All other currencies in the release are quoted in currency units per US dollar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is there no H.10 rate for some dates?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No rate is published on US federal holidays or weekends. The Fed's files mark those dates ND for no data. Use the last available rate before the date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use H.10 rates for the FBAR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. The FBAR and Form 8938 instructions point to the Treasury rate for the last day of the year. H.10 is the usual source for transaction-date rates and for computing averages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://allratestoday.com/blog/fed-h10-exchange-rate-history-download/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt;. The Treasury, Federal Reserve and IRS tables used above are served as JSON, CSV, XML and Excel at &lt;a href="https://allratestoday.com/tax-authority-rates-api/" rel="noopener noreferrer"&gt;allratestoday.com/tax-authority-rates-api/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>finance</category>
      <category>api</category>
    </item>
    <item>
      <title>Form 5471 Exchange Rates: Average Rate, Year-End Rate and the Divide-By Convention by Schedule</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:02:50 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/form-5471-exchange-rates-average-rate-year-end-rate-and-the-divide-by-convention-by-schedule-5274</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/form-5471-exchange-rates-average-rate-year-end-rate-and-the-divide-by-convention-by-schedule-5274</guid>
      <description>&lt;p&gt;Form 5471 is the information return for US persons with interests in foreign corporations. Its schedules mix functional-currency columns and dollar columns, and the instructions do not use one exchange rate throughout. Preparers lose time on two things: which rate each schedule wants, and how the rate itself must be written on the form. This guide maps both, from the Instructions for Form 5471 (revision December 2025).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Not tax advice.&lt;/strong&gt; This is a practical summary for people who prepare the spreadsheets, scripts and working papers. Quotations are from the IRS, Treasury and Federal Reserve texts current in November 2026; read the instructions for your own tax year and confirm your case with a US tax adviser.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How the rate must be reported: the divide-by convention
&lt;/h2&gt;

&lt;p&gt;The instructions are explicit: "all exchange rates must be reported using a 'divide-by convention' rounded to at least four places." That is, "the exchange rate must be reported as the units of foreign currency that equal one U.S. dollar, rounded to at least four places. Do not report the exchange rate as the number of U.S. dollars that equal one unit of foreign currency." You must round to more than four places if four would materially distort the rate or the dollar amount.&lt;/p&gt;

&lt;p&gt;This trips up anyone taking euro, sterling, Australian dollar or New Zealand dollar rates from the Federal Reserve, which quotes those four in dollars per unit. They have to be inverted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source figure&lt;/th&gt;
&lt;th&gt;As quoted&lt;/th&gt;
&lt;th&gt;Divide-by rate for Form 5471&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fed H.10, EUR, average of 2025 daily rates&lt;/td&gt;
&lt;td&gt;1.1306 USD per EUR&lt;/td&gt;
&lt;td&gt;0.8845&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fed H.10, EUR, December 31, 2025&lt;/td&gt;
&lt;td&gt;1.1736 USD per EUR&lt;/td&gt;
&lt;td&gt;0.8521&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fed H.10, GBP, average of 2025 daily rates&lt;/td&gt;
&lt;td&gt;1.3192 USD per GBP&lt;/td&gt;
&lt;td&gt;0.7580&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fed H.10, JPY, average of 2025 daily rates&lt;/td&gt;
&lt;td&gt;149.5686 JPY per USD&lt;/td&gt;
&lt;td&gt;149.5686&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treasury and IRS tables are already in units per dollar, but they are published to three decimals (0.851, 0.886), which does not meet "at least four places" on its own. A daily series you can average and round yourself is the practical source.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which rate on which schedule
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Schedule&lt;/th&gt;
&lt;th&gt;What the instructions say&lt;/th&gt;
&lt;th&gt;Rate in practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;C&lt;/strong&gt;, income statement&lt;/td&gt;
&lt;td&gt;Report in functional currency in accordance with US GAAP "and translate using U.S. GAAP translation principles"&lt;/td&gt;
&lt;td&gt;ASC 830: rates at the dates items are recognized, usually a weighted average for the period&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;F&lt;/strong&gt;, balance sheet&lt;/td&gt;
&lt;td&gt;Report in US dollars; the balance sheet is generally "translated to U.S. dollars using U.S. GAAP translation rules"&lt;/td&gt;
&lt;td&gt;ASC 830: the current rate at the balance sheet date for assets and liabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;E&lt;/strong&gt;, foreign taxes&lt;/td&gt;
&lt;td&gt;Translate "at the average exchange rate for the tax year to which the tax relates unless one of the exceptions below applies. See section 986(a)."&lt;/td&gt;
&lt;td&gt;Average for the year; payment-date rate for the exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;H&lt;/strong&gt;, current earnings and profits, line 5d&lt;/td&gt;
&lt;td&gt;Translate "at the average exchange rate for the foreign corporation's tax year. See section 989(b)."&lt;/td&gt;
&lt;td&gt;Average for the corporation's tax year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;M&lt;/strong&gt;, related-party transactions&lt;/td&gt;
&lt;td&gt;"Use the average exchange rate for the foreign corporation's tax year. See section 989(b)."&lt;/td&gt;
&lt;td&gt;Average for the corporation's tax year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;I-1&lt;/strong&gt;, GILTI information&lt;/td&gt;
&lt;td&gt;Average rate for the CFC's year&lt;/td&gt;
&lt;td&gt;Average for the corporation's tax year&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Schedule E exceptions mirror section 986(a): taxes paid before the year begins, taxes not paid within 2 years, a section 986(a)(1)(D) election, a cash-basis corporation, and inflationary currencies all use the payment-date rate. Corporations using the dollar approximate separate transactions method follow Reg. section 1.985-3 for Schedule F instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Section 989(b) in two lines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Actual distributions of earnings: "the spot rate on the date such distribution is included in income."&lt;/li&gt;
&lt;li&gt;Subpart F inclusions under section 951(a)(1)(A): "the average exchange rate for the taxable year of the foreign corporation."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a dividend paid on a specific day needs that day's rate, while the year's inclusion uses the year's average.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "average for the tax year" means for a fiscal-year corporation
&lt;/h2&gt;

&lt;p&gt;The average is for the &lt;em&gt;foreign corporation's&lt;/em&gt; tax year, not the calendar year and not the US shareholder's year. A German subsidiary with a year ending September 30, 2025 needs the average of October 1, 2024 to September 30, 2025. The IRS calendar-year average table does not cover that; you compute it from a daily series:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://raw.githubusercontent.com/AllRates-Today/central-bank-exchange-rates/main/data/fed/history/{}.csv"&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_dates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;eur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;            &lt;span class="c1"&gt;# USD per EUR&lt;/span&gt;
&lt;span class="n"&gt;fy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eur&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2024-10-01"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2025-09-30"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                &lt;span class="c1"&gt;# divide-by rate, units per USD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State in the working papers that the average is the simple mean of the daily Federal Reserve H.10 rates for the business days of the period, and use the same construction every year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calendar 2025 reference rates
&lt;/h2&gt;

&lt;p&gt;Units of foreign currency per one US dollar, in divide-by form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Currency&lt;/th&gt;
&lt;th&gt;2025 average (Fed H.10 daily mean)&lt;/th&gt;
&lt;th&gt;IRS 2025 yearly average&lt;/th&gt;
&lt;th&gt;Dec 31, 2025 (Fed H.10)&lt;/th&gt;
&lt;th&gt;Dec 31, 2025 (Treasury)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EUR&lt;/td&gt;
&lt;td&gt;0.8845&lt;/td&gt;
&lt;td&gt;0.886&lt;/td&gt;
&lt;td&gt;0.8521&lt;/td&gt;
&lt;td&gt;0.851&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GBP&lt;/td&gt;
&lt;td&gt;0.7580&lt;/td&gt;
&lt;td&gt;0.759&lt;/td&gt;
&lt;td&gt;0.7437&lt;/td&gt;
&lt;td&gt;0.743&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAD&lt;/td&gt;
&lt;td&gt;1.3973&lt;/td&gt;
&lt;td&gt;1.398&lt;/td&gt;
&lt;td&gt;1.3712&lt;/td&gt;
&lt;td&gt;1.369&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPY&lt;/td&gt;
&lt;td&gt;149.5686&lt;/td&gt;
&lt;td&gt;149.632&lt;/td&gt;
&lt;td&gt;156.8000&lt;/td&gt;
&lt;td&gt;156.61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CHF&lt;/td&gt;
&lt;td&gt;0.8305&lt;/td&gt;
&lt;td&gt;0.831&lt;/td&gt;
&lt;td&gt;0.7936&lt;/td&gt;
&lt;td&gt;0.792&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INR&lt;/td&gt;
&lt;td&gt;87.1468&lt;/td&gt;
&lt;td&gt;87.133&lt;/td&gt;
&lt;td&gt;89.8400&lt;/td&gt;
&lt;td&gt;89.854&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MXN&lt;/td&gt;
&lt;td&gt;19.2052&lt;/td&gt;
&lt;td&gt;19.212&lt;/td&gt;
&lt;td&gt;18.0057&lt;/td&gt;
&lt;td&gt;17.956&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CNY&lt;/td&gt;
&lt;td&gt;7.1875&lt;/td&gt;
&lt;td&gt;7.129&lt;/td&gt;
&lt;td&gt;6.9931&lt;/td&gt;
&lt;td&gt;6.998&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The year-end sources do not agree to the last digit: Treasury's quarterly table reflects rates reported for the last business day of the prior month, while H.10 is the noon buying rate on the day. Pick the source that matches your GAAP translation policy and use it for both the financial statements and Schedule F.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling the rates
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# the H.10 table for a balance sheet date&lt;/span&gt;
curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/fed/2025-12-31"&lt;/span&gt;

&lt;span class="c"&gt;# a daily series for a fiscal-year average&lt;/span&gt;
curl -H &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt; \
  &lt;span class="s2"&gt;"https://allratestoday.com/api/v1/central-bank/fed/history?symbol=EUR&amp;amp;from=2024-10-01&amp;amp;to=2025-09-30"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every table endpoint returns JSON, CSV, XML or Excel through &lt;code&gt;?format=&lt;/code&gt;. Dated tables and history are on paid plans. For currencies outside the Federal Reserve's list, the same endpoints serve each country's own central bank table.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How must exchange rates be reported on Form 5471?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using the divide-by convention: units of foreign currency that equal one US dollar, rounded to at least four places. The instructions say not to report the number of US dollars per unit of foreign currency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which exchange rate does Schedule H use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Line 5d of Schedule H translates current earnings and profits at the average exchange rate for the foreign corporation's tax year, citing section 989(b).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which exchange rate does Schedule F use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Schedule F is reported in US dollars and the balance sheet is generally translated using US GAAP translation rules, which means the current rate at the balance sheet date for assets and liabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which rate applies to foreign taxes on Schedule E?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The average exchange rate for the tax year to which the tax relates, under section 986(a), unless an exception applies. The exceptions, such as taxes not paid within 2 years or a cash-basis corporation, use the payment-date rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the IRS yearly average table for a fiscal-year foreign corporation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The IRS table is a calendar-year average published to three decimals. A corporation with a different tax year needs an average over its own year, computed from a daily series, and Form 5471 asks for at least four places.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://allratestoday.com/blog/form-5471-exchange-rates-average-year-end/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt;. The Treasury, Federal Reserve and IRS tables used above are served as JSON, CSV, XML and Excel at &lt;a href="https://allratestoday.com/tax-authority-rates-api/" rel="noopener noreferrer"&gt;allratestoday.com/tax-authority-rates-api/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tax</category>
      <category>accounting</category>
      <category>usa</category>
      <category>api</category>
    </item>
    <item>
      <title>20 Currency &amp; Exchange Rate API Questions Answered (2026)</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Thu, 28 May 2026 08:46:18 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/20-currency-amp-exchange-rate-api-questions-answered-2026-mdash-exchange-rate-api-59c9</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/20-currency-amp-exchange-rate-api-questions-answered-2026-mdash-exchange-rate-api-59c9</guid>
      <description>&lt;p&gt;Developers keep searching for the same set of questions about currency APIs, exchange rate APIs, and forex APIs: which ones are free, which are reliable, what Xe costs, whether Google has an API, how to build a converter in Python. We pulled the 20 most-asked from Google's People Also Ask and answered each one directly -- no fluff, no affiliate padding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Is there a free API for currency conversion?&lt;/li&gt;
&lt;li&gt;What is a currency API?&lt;/li&gt;
&lt;li&gt;What is a forex API?&lt;/li&gt;
&lt;li&gt;What is an FX API?&lt;/li&gt;
&lt;li&gt;What is an open exchange rate?&lt;/li&gt;
&lt;li&gt;Is the Frankfurter API free?&lt;/li&gt;
&lt;li&gt;Is an exchange rate API free?&lt;/li&gt;
&lt;li&gt;Is ExchangeRate-API free?&lt;/li&gt;
&lt;li&gt;How much does ExchangeRate-API cost?&lt;/li&gt;
&lt;li&gt;Does Xe have an API?&lt;/li&gt;
&lt;li&gt;Is the Xe API free?&lt;/li&gt;
&lt;li&gt;Is the Google Currency Converter API free?&lt;/li&gt;
&lt;li&gt;Is Fixer.io reliable?&lt;/li&gt;
&lt;li&gt;Is the MT5 API free?&lt;/li&gt;
&lt;li&gt;Building a currency converter in Python?&lt;/li&gt;
&lt;li&gt;What is the formula to calculate an exchange rate?&lt;/li&gt;
&lt;li&gt;What is the GOOGLEFINANCE function?&lt;/li&gt;
&lt;li&gt;Which is the best currency converter?&lt;/li&gt;
&lt;li&gt;Best currency converter app?&lt;/li&gt;
&lt;li&gt;Best rate when converting currency?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Q1.Is there a free API for currency conversion?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes -- several.&lt;/strong&gt; The main free options in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://api.frankfurter.dev/" rel="noopener noreferrer"&gt;Frankfurter&lt;/a&gt; -- 100% free, no API key, ECB daily reference rates for ~30 currencies, historical back to 1999.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/fawazahmed0/currency-api" rel="noopener noreferrer"&gt;fawazahmed0/currency-api&lt;/a&gt; -- free via jsDelivr CDN, 200+ currencies including crypto.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.exchangerate-api.com/" rel="noopener noreferrer"&gt;ExchangeRate-API&lt;/a&gt; -- 1,500 requests/month on the free tier (key required).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://freecurrencyapi.com/" rel="noopener noreferrer"&gt;freecurrencyapi.com&lt;/a&gt; -- 5,000 requests/month.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://allratestoday.com/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt; -- free API key for real-time rates, plus the official tables of 123 central banks with no key at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For deeper coverage of the trade-offs, see &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;our guide to the best free currency exchange APIs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q2.What is a currency API?
&lt;/h2&gt;

&lt;p&gt;A currency API is a REST web service that returns exchange rates between currencies, usually as JSON. A typical call:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR
-&amp;gt; {"rate": 0.8628, "source": "USD", "target": "EUR", "time": "2026-11-09T12:00:00Z"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Currency APIs power e-commerce checkout, finance dashboards, accounting tools, travel apps, international payroll, and any software that displays money in more than one currency. Full background in &lt;a href="https://allratestoday.com/blog/what-is-a-currency-api/" rel="noopener noreferrer"&gt;What is a currency API?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q3.What is a forex API?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;forex (FX) API&lt;/strong&gt; is a currency API built for trading rather than display. Expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tick-level &lt;strong&gt;bid/ask quotes&lt;/strong&gt; with millisecond latency (not just a mid-market rate)&lt;/li&gt;
&lt;li&gt;Historical OHLC (open/high/low/close) data for charting&lt;/li&gt;
&lt;li&gt;Sometimes &lt;strong&gt;order execution&lt;/strong&gt; endpoints -- place, modify, cancel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Examples: OANDA, MetaTrader (MT4/MT5), TraderMade, Polygon.io, FXCM. General-purpose currency APIs only return reference mid-market rates and are not suitable for trading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q4.What is an FX API?
&lt;/h2&gt;

&lt;p&gt;"FX API" is another name for "forex API." Some enterprise providers -- banks, Stripe's FX product, Bloomberg, OCBC -- reserve "FX API" for streaming quotes and execution services covering 100+ pairs. In casual developer conversation, "currency API" and "FX API" are often used interchangeably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q5.What is an open exchange rate?
&lt;/h2&gt;

&lt;p&gt;Two meanings:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generic:&lt;/strong&gt; an exchange rate openly accessible through a public API -- no private contract needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specific:&lt;/strong&gt; &lt;a href="https://openexchangerates.org/" rel="noopener noreferrer"&gt;Open Exchange Rates&lt;/a&gt; is the name of a commercial provider publishing rates for 200+ currencies since 2011. Free tier: 1,000 requests/month, hourly updates, USD base only.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Q6.Is the Frankfurter API free?
&lt;/h2&gt;

&lt;p&gt;Yes. &lt;a href="https://www.frankfurter.dev/" rel="noopener noreferrer"&gt;Frankfurter&lt;/a&gt; is a free, open-source currency data API with no key, no signup, and no documented rate limit for normal use. It tracks ~30 currencies using European Central Bank daily reference rates and supports historical data back to 1999. Because it's ECB-based, rates update once per business day around 16:00 CET -- great for accounting and display, not for trading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q7.Is an exchange rate API free?
&lt;/h2&gt;

&lt;p&gt;Depends on the provider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Key?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frankfurter&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fawazahmed0&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://allratestoday.com/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free API key; official central bank tables keyless&lt;/td&gt;
&lt;td&gt;Market rates yes, official tables no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate-API&lt;/td&gt;
&lt;td&gt;1,500/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;freecurrencyapi.com&lt;/td&gt;
&lt;td&gt;5,000/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Exchange Rates&lt;/td&gt;
&lt;td&gt;1,000/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixer.io&lt;/td&gt;
&lt;td&gt;100/mo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Xe / OANDA / Bloomberg&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;See &lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;Best free currency exchange API (2026)&lt;/a&gt; for a detailed comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q8.Is ExchangeRate-API free?
&lt;/h2&gt;

&lt;p&gt;ExchangeRate-API has a free plan of &lt;strong&gt;1,500 requests/month&lt;/strong&gt; , no credit card required. Fine for hobby projects, prototypes, and small internal tools. The terms of service ask commercial users to upgrade. Also note that the rate is ExchangeRate-API's own proprietary blend of undisclosed sources: not a central bank rate, not a standardised fixing, so it cannot be reproduced or cited in a filing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q9.How much does ExchangeRate-API cost?
&lt;/h2&gt;

&lt;p&gt;Paid plans as of 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pro&lt;/strong&gt; -- $13.99/month, 30,000 requests/month, hourly updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business&lt;/strong&gt; -- $29/month, 100,000 requests/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume&lt;/strong&gt; -- $49/month, 1.5M requests/month, minute-level updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise&lt;/strong&gt; -- custom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices change occasionally; check &lt;a href="https://www.exchangerate-api.com/" rel="noopener noreferrer"&gt;their pricing page&lt;/a&gt; for current numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q10.Does Xe have an API?
&lt;/h2&gt;

&lt;p&gt;Yes. &lt;a href="https://www.xe.com/platform/xecurrencydata/" rel="noopener noreferrer"&gt;Xe Currency Data API&lt;/a&gt; provides real-time and historical exchange rates for 170+ currencies. It's used by Fortune 500 companies, banks, and fintechs, with optional SLAs and uptime guarantees. Full breakdown in &lt;a href="https://allratestoday.com/blog/does-xe-have-an-api/" rel="noopener noreferrer"&gt;Does Xe have an API?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q11.Is the Xe API free?
&lt;/h2&gt;

&lt;p&gt;No. Xe has no public free tier. Pricing is enterprise-only and typically starts around &lt;strong&gt;$799/month&lt;/strong&gt; depending on volume and features. For free access, Frankfurter, ExchangeRate-API's free tier, or an &lt;a href="https://allratestoday.com/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt; free API key are the common alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q12.Is the Google Currency Converter API free?
&lt;/h2&gt;

&lt;p&gt;There is no official public Google Currency API (the old Google Finance API was retired in 2012). What exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;GOOGLEFINANCE&lt;/code&gt; function in Google Sheets -- free, ~20-minute delay, not a REST API.&lt;/li&gt;
&lt;li&gt;Unofficial wrappers (e.g. &lt;code&gt;currency-api.appspot.com&lt;/code&gt;) -- community-hosted, no SLA, can disappear without warning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't depend on Google for production currency rates. Use a supported provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q13.Is Fixer.io reliable?
&lt;/h2&gt;

&lt;p&gt;Yes, within its bounds. Fixer.io is owned by &lt;strong&gt;apilayer&lt;/strong&gt; and has been running since 2015. Paid plans have uptime guarantees and are generally reliable. Limitations on the free tier are heavy: 100 requests/month, HTTP only (no HTTPS), and EUR is the only allowed base currency. Most production users end up on Basic ($14.99/month) or Professional ($59.99/month). Side-by-side: &lt;a href="https://allratestoday.com/compare/fixer-alternative/" rel="noopener noreferrer"&gt;AllRatesToday vs Fixer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q14.Is the MT5 API free?
&lt;/h2&gt;

&lt;p&gt;No. MetaTrader 5 Manager and Gateway APIs require a licensed MT5 server and a paid integration agreement with MetaQuotes. Brokers may offer trial access to demo accounts. If you only need exchange rates (not trade execution), a plain currency API is dramatically simpler and cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q15.How do you make a currency converter in Python with an API?
&lt;/h2&gt;

&lt;p&gt;Minimal working example using Frankfurter (no key needed):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests

def convert(amount, src, dst):
    r = requests.get("https://api.frankfurter.dev/v1/latest",
                     params={"from": src, "to": dst})
    r.raise_for_status()
    rate = r.json()["rates"][dst]
    return amount * rate, rate

amount, rate = convert(100, "USD", "EUR")
print(f"100 USD = {amount:.2f} EUR  (rate {rate})")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Add error handling for timeouts and unknown currency codes, cache results for 30-60 minutes, and you have a production-ready converter. Deeper walk-through in &lt;a href="https://allratestoday.com/blog/how-to-get-live-exchange-rates-python/" rel="noopener noreferrer"&gt;How to get live exchange rates in Python&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q16.What is the formula to calculate an exchange rate?
&lt;/h2&gt;

&lt;p&gt;The exchange rate is the ratio of two currency amounts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rate = target_amount / source_amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If 100 USD buys 85 EUR, the USD-&amp;gt;EUR rate is &lt;code&gt;85 / 100 = 0.85&lt;/code&gt;. To convert any amount, multiply by the rate:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;250 USD × 0.85 = 212.50 EUR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The inverse (EUR-&amp;gt;USD) is &lt;code&gt;1 / 0.85 ≈ 1.176&lt;/code&gt;. Real-world rates include a spread -- the "buy" and "sell" rates differ slightly, and that gap is the provider's margin. More: &lt;a href="https://allratestoday.com/blog/mid-market-vs-retail-exchange-rates/" rel="noopener noreferrer"&gt;Mid-market vs retail exchange rates&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q17.What is the GOOGLEFINANCE function for exchange rates?
&lt;/h2&gt;

&lt;p&gt;In Google Sheets:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=GOOGLEFINANCE("CURRENCY:USDEUR")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Replace &lt;code&gt;USDEUR&lt;/code&gt; with any two concatenated ISO 4217 codes (&lt;code&gt;GBPJPY&lt;/code&gt;, &lt;code&gt;AUDCAD&lt;/code&gt;, etc.). For historical rates, add a date:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=GOOGLEFINANCE("CURRENCY:USDEUR", "price", DATE(2024,1,1))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Rates are delayed ~20 minutes. Fine for spreadsheets; don't build production systems on it. For Excel, see &lt;a href="https://allratestoday.com/blog/excel-exchange-rates/" rel="noopener noreferrer"&gt;How to get exchange rates in Excel&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q18.Which is the best currency converter?
&lt;/h2&gt;

&lt;p&gt;Depends on the use case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending money abroad:&lt;/strong&gt; &lt;a href="https://wise.com/" rel="noopener noreferrer"&gt;Wise&lt;/a&gt; -- real mid-market rate, transparent fees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick lookup on web/mobile:&lt;/strong&gt; Google search or &lt;a href="https://xe.com/" rel="noopener noreferrer"&gt;XE.com&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding in software:&lt;/strong&gt; &lt;a href="https://allratestoday.com/" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt; for real-time plus official rates, or Frankfurter for ECB only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise / trading:&lt;/strong&gt; Xe, OANDA, or Bloomberg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For developers comparing providers head-to-head, see &lt;a href="https://allratestoday.com/best/best-exchange-rate-api/" rel="noopener noreferrer"&gt;best exchange rate API comparison&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q19.What is the best currency converter app to use?
&lt;/h2&gt;

&lt;p&gt;Most-recommended consumer apps in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wise&lt;/strong&gt; -- best mid-market rate + actual money transfer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XE Currency&lt;/strong&gt; -- clean UI, offline mode for travel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revolut&lt;/strong&gt; -- multi-currency account with in-app conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency Converter Plus&lt;/strong&gt; -- simple free converter (ad-supported).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Q20.Where should you convert currency for the best exchange rate?
&lt;/h2&gt;

&lt;p&gt;You get closest to the mid-market rate with online-first providers: &lt;strong&gt;Wise&lt;/strong&gt; , &lt;strong&gt;Revolut&lt;/strong&gt; , &lt;strong&gt;Currencies Direct&lt;/strong&gt; , or &lt;strong&gt;OFX&lt;/strong&gt;. Traditional banks and airport kiosks mark up the rate by 2-5% on top of a fee. Travel-friendly credit cards (no foreign transaction fee) are good for purchases abroad; avoid "Dynamic Currency Conversion" at the point of sale -- always pay in the local currency.&lt;/p&gt;

&lt;p&gt;Rule of thumb: check the mid-market rate first (Google, AllRatesToday, XE), then compare the rate you're offered. Any gap is the provider's margin.&lt;/p&gt;




&lt;p&gt;Ready to integrate a reliable currency API? &lt;a href="https://allratestoday.com/register/" rel="noopener noreferrer"&gt;Get your free API key&lt;/a&gt; -- no credit card required. Official central bank tables need no key at all: &lt;code&gt;https://allratestoday.com/api/open/central-bank/ecb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Get started&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @allratestoday/sdk&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Related Articles&lt;br&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/best/best-currency-api-2026/" rel="noopener noreferrer"&gt;Best currency API 2026: buyer's guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/exchange_rateapi_ee936ad/10-best-currency-exchange-api-options-for-developers-2026-1284"&gt;10 best currency exchange APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/free-vs-paid-exchange-rate-apis/" rel="noopener noreferrer"&gt;Free vs paid exchange rate APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/central-bank-rates-api/institutions/" rel="noopener noreferrer"&gt;Central bank rates API: 123 institutions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>currency</category>
      <category>api</category>
      <category>webdev</category>
      <category>fintech</category>
    </item>
    <item>
      <title>10 Best Currency Exchange API Options for Developers (2026)</title>
      <dc:creator>Exchange Rate API</dc:creator>
      <pubDate>Thu, 28 May 2026 08:45:50 +0000</pubDate>
      <link>https://dev.to/exchange_rateapi_ee936ad/10-best-currency-exchange-api-options-for-developers-2026-1284</link>
      <guid>https://dev.to/exchange_rateapi_ee936ad/10-best-currency-exchange-api-options-for-developers-2026-1284</guid>
      <description>&lt;p&gt;If you are building a financial app, an e-commerce checkout, or an analytics dashboard, you need reliable exchange rate data. Hard-coding rates or scraping them from a website is slow, fragile, and a support nightmare. A &lt;strong&gt;currency exchange API&lt;/strong&gt; solves this by delivering live, accurate FX data to your application on demand.&lt;/p&gt;

&lt;p&gt;This guide explains what a currency conversion API is, how it works, which features actually matter, and compares the &lt;strong&gt;10 best currency exchange APIs&lt;/strong&gt; available to developers in 2026 -- starting with &lt;strong&gt;AllRatesToday&lt;/strong&gt;, which is the only one in the list that serves both real-time market rates and the official daily tables of 123 central banks, with SDKs in five languages and a free API key that needs no credit card.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Currency Conversion API?
&lt;/h2&gt;

&lt;p&gt;A currency conversion API is a web service that returns live or historical exchange rate data over HTTPS in a machine-readable format -- usually JSON. Your application sends a request (e.g. "USD to EUR"), and the API responds with the current mid-market rate pulled from its data feeds.&lt;/p&gt;

&lt;p&gt;Instead of maintaining your own pipeline of bank feeds, FX aggregators, and crypto exchanges, you integrate one REST endpoint and let the provider handle the data plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Need a Currency API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Localized pricing&lt;/strong&gt; -- display product prices in the visitor's local currency to reduce cart abandonment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-currency billing&lt;/strong&gt; -- invoice SaaS customers in their own currency while reporting in a single base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Financial reporting&lt;/strong&gt; -- convert foreign transactions to the reporting currency at historical rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trading and analytics&lt;/strong&gt; -- feed dashboards, screeners, and backtests with live FX and crypto data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-border payouts&lt;/strong&gt; -- calculate accurate settlement amounts for marketplaces and payroll systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How a Currency API Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Your application sends an HTTPS request with a source currency, target currencies, and an API key.&lt;/li&gt;
&lt;li&gt;The provider's server looks up the latest rates from aggregated data sources (banks, interbank feeds, exchanges).&lt;/li&gt;
&lt;li&gt;The server returns a JSON response with the requested rates and a timestamp.&lt;/li&gt;
&lt;li&gt;Your application caches or uses the rate to convert amounts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Common Endpoint Types
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latest Rates&lt;/strong&gt; -- the most recent exchange rates for one or many currency pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical Rates&lt;/strong&gt; -- rates on a specific past date, useful for accounting and charting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Series&lt;/strong&gt; -- a range of daily rates between two dates for trend analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert&lt;/strong&gt; -- a convenience endpoint that does the math for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fluctuation&lt;/strong&gt; -- the change, percentage change, and high/low over a period.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Makes a Great Currency API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Traceable data sources&lt;/strong&gt; -- a market mid-rate you can reconcile, or an official central bank table you can cite. Avoid proprietary blends with undisclosed weighting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update frequency&lt;/strong&gt; -- real-time tiers drop to 60 seconds or faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage&lt;/strong&gt; -- 150+ fiat currencies plus metals and crypto where relevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent pricing&lt;/strong&gt; -- a real free tier, no hidden fees, no credit-card walls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official SDKs&lt;/strong&gt; -- so you don't write HTTP plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean JSON responses&lt;/strong&gt; and strong documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt; -- HTTPS, scoped API keys, reasonable rate limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 10 Best Currency Exchange APIs in 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AllRatesToday -- The Best Currency Exchange API for Developers
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://allratestoday.com" rel="noopener noreferrer"&gt;AllRatesToday&lt;/a&gt; delivers real-time mid-market exchange rates for &lt;strong&gt;160+ currencies&lt;/strong&gt;, refreshed every 60 seconds, and alongside them the &lt;strong&gt;official daily tables of 123 central banks and 4 tax authorities&lt;/strong&gt; (ECB, Fed, RBI, SBI, BoE, SNB, CBUAE, SAMA, IRS yearly averages and more). It is purpose-built for developers: a clean REST API, official SDKs for JavaScript, Python, PHP, Go and Java, an MCP server, drop-in compatible endpoints for Fixer and exchangeratesapi.io, and a free API key that does not ask for a credit card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highlights:&lt;/strong&gt; 160+ currencies, 60-second updates, 123 official central bank tables (keyless), SDKs in 5 languages, JSON/CSV/XML/XLSX output, history and commercial use on every paid plan from €4.99/month.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# real-time market rate
curl "https://allratestoday.com/api/v1/rates?source=USD&amp;amp;target=EUR" \
  -H "Authorization: Bearer YOUR_API_KEY"

# official ECB table, no key needed
curl "https://allratestoday.com/api/open/central-bank/ecb"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt; -- fintech apps, SaaS billing, e-commerce localization, accounting and tax tooling, and any developer who needs a rate that is fresh now and defensible later.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CurrencyFreaks
&lt;/h3&gt;

&lt;p&gt;Covers 1,000+ symbols including fiats, metals, and hundreds of cryptocurrencies. Update frequency ranges from 24 hours on the free plan down to 60 seconds on the Professional tier. A solid all-rounder if breadth of crypto coverage matters more than depth of FX sourcing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fixer
&lt;/h3&gt;

&lt;p&gt;Delivers FX rates through a clean JSON API, with historical data going back to 1999 and sourcing aligned with the European Central Bank. The free plan is capped at 100 calls/month with hourly updates -- suitable mainly for prototypes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. ExchangeRate-API
&lt;/h3&gt;

&lt;p&gt;Simple API with 161 currencies and a 1,500 request/month free tier. Paid plans bring hourly and 5-minute updates. Note that its rate is a proprietary blend with undisclosed weighting: it is not a central bank rate or a standardised fixing, so it cannot be reproduced or cited.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Open Exchange Rates
&lt;/h3&gt;

&lt;p&gt;A long-running provider with 200+ currencies and a large ecosystem of open-source client libraries. The free tier is hourly-updated and capped at 1,000 requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. CurrencyLayer
&lt;/h3&gt;

&lt;p&gt;Part of the APILayer network; 168 currencies and metals with a familiar endpoint structure. Free tier is limited to 100 calls/month.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. XE
&lt;/h3&gt;

&lt;p&gt;A well-known brand with 30+ years of FX history and 170+ currencies. Pricing is annual and geared toward enterprises rather than indie developers, with plans starting in the high hundreds per year.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. CurrencyAPI
&lt;/h3&gt;

&lt;p&gt;Straightforward REST API covering 170+ fiat currencies and cryptocurrencies, with sandbox access on paid tiers and 60-second updates at higher plans.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Alpha Vantage
&lt;/h3&gt;

&lt;p&gt;Broader market-data API covering stocks, forex, crypto, ETFs, and commodities, plus 60+ technical indicators. Rate limits on the free tier are low but cross-asset coverage is unmatched in this list.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. ExchangeRate.host
&lt;/h3&gt;

&lt;p&gt;Simple, fast forex and crypto API with 19 years of historical data, 168+ symbols, and a free plan. Paid tiers unlock higher request limits and faster updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;Currencies&lt;/th&gt;
&lt;th&gt;Update Frequency&lt;/th&gt;
&lt;th&gt;Credit Card&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AllRatesToday&lt;/td&gt;
&lt;td&gt;Free API key + keyless official tables&lt;/td&gt;
&lt;td&gt;160+&lt;/td&gt;
&lt;td&gt;Real-time (60s)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurrencyFreaks&lt;/td&gt;
&lt;td&gt;1,000 req/month&lt;/td&gt;
&lt;td&gt;1,000+&lt;/td&gt;
&lt;td&gt;Daily (free)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixer&lt;/td&gt;
&lt;td&gt;100 req/month&lt;/td&gt;
&lt;td&gt;170+&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate-API&lt;/td&gt;
&lt;td&gt;1,500 req/month&lt;/td&gt;
&lt;td&gt;161&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Yes (upgrades)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Exchange Rates&lt;/td&gt;
&lt;td&gt;1,000 req/month&lt;/td&gt;
&lt;td&gt;200+&lt;/td&gt;
&lt;td&gt;Hourly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurrencyLayer&lt;/td&gt;
&lt;td&gt;100 req/month&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XE&lt;/td&gt;
&lt;td&gt;Free trial&lt;/td&gt;
&lt;td&gt;170+&lt;/td&gt;
&lt;td&gt;Up to 60s&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurrencyAPI&lt;/td&gt;
&lt;td&gt;300 req/month&lt;/td&gt;
&lt;td&gt;170+&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha Vantage&lt;/td&gt;
&lt;td&gt;25 req/day&lt;/td&gt;
&lt;td&gt;Multi-asset&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExchangeRate.host&lt;/td&gt;
&lt;td&gt;Free plan&lt;/td&gt;
&lt;td&gt;168+&lt;/td&gt;
&lt;td&gt;Daily&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to Choose the Right API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time trading or pricing&lt;/strong&gt; -- pick a provider with 60-second updates (AllRatesToday, CurrencyFreaks Pro, XE).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounting and reporting&lt;/strong&gt; -- historical endpoints and citable sources matter more than latency. AllRatesToday is the only entry here with official central bank tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crypto-first apps&lt;/strong&gt; -- CurrencyFreaks or CurrencyAPI for breadth; AllRatesToday if you need FX plus official rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototypes and hobby projects&lt;/strong&gt; -- prefer free tiers without credit-card walls (AllRatesToday, ExchangeRate.host).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-asset dashboards&lt;/strong&gt; -- Alpha Vantage is the clear choice for equities + FX + crypto in one API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started with AllRatesToday
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Register at &lt;a href="https://allratestoday.com/register/" rel="noopener noreferrer"&gt;allratestoday.com/register&lt;/a&gt; -- no credit card required.&lt;/li&gt;
&lt;li&gt;Copy your API key from the dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install an SDK or use &lt;code&gt;curl&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;npm install @allratestoday/sdk&lt;/p&gt;

&lt;p&gt;import AllRatesToday from '@allratestoday/sdk';&lt;/p&gt;

&lt;p&gt;const client = new AllRatesToday({ apiKey: process.env.ALLRATES_API_KEY });&lt;br&gt;
const { rate } = await client.getRate('USD', 'EUR');&lt;br&gt;
console.log(&lt;code&gt;1 USD = ${rate} EUR&lt;/code&gt;);&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;That is it&lt;/strong&gt; -- live FX data in your application, with the official central bank tables one keyless call away.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I start with a free currency API?
&lt;/h3&gt;

&lt;p&gt;Sign up for the provider's free plan, get an API key, and send a request to the latest-rates endpoint using &lt;code&gt;curl&lt;/code&gt; or your language's HTTP client.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which languages can I use?
&lt;/h3&gt;

&lt;p&gt;Any language that can make an HTTPS request. AllRatesToday ships official SDKs for JavaScript/TypeScript, Python, PHP, Go and Java.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I get both real-time and historical rates?
&lt;/h3&gt;

&lt;p&gt;Yes -- quality APIs expose both. Historical endpoints usually accept a &lt;code&gt;YYYY-MM-DD&lt;/code&gt; date parameter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do free tiers support cryptocurrency?
&lt;/h3&gt;

&lt;p&gt;Some do (CurrencyFreaks, CurrencyAPI). Always check the documentation for supported symbols before committing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often are rates updated?
&lt;/h3&gt;

&lt;p&gt;Free tiers typically refresh daily or hourly. Paid tiers drop to 60 seconds or faster. AllRatesToday updates every 60 seconds; its official central bank tables update once per business day when each bank publishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/best-free-currency-exchange-api-2026/" rel="noopener noreferrer"&gt;Best free currency exchange API (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/best-realtime-forex-api-2026/" rel="noopener noreferrer"&gt;Best real-time forex API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/blog/free-vs-paid-exchange-rate-apis/" rel="noopener noreferrer"&gt;Free vs paid exchange rate APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/best/best-currency-api-2026/" rel="noopener noreferrer"&gt;Best currency API 2026: buyer's guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://allratestoday.com/central-bank-rates-api/institutions/" rel="noopener noreferrer"&gt;Central bank rates API: 123 institutions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>currency</category>
      <category>api</category>
      <category>webdev</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
