Skip to content

Exchangeratesapi.io ​

exchangeratesapi.io (an APILayer product) serves current and historical foreign exchange rates.

WARNING

On the free tier, use Frankfurter instead. exchangeratesapi.io's free tier serves the ECB reference rates with base EUR only — the same numbers Frankfurter serves with no API key at all. This source is only worth its key on a paid plan, which adds a switchable base currency, ~172 currencies, and — the reason it exists here — weekend rows. The ECB does not publish on weekends, so Frankfurter has no Saturday or Sunday rates. If your models join FX on an exact date rather than as-of, that gap becomes a zero rather than a carried-forward rate.

CAUTION

Historical rates are not reproducible, so do not use this source to rebuild history. The API answers with what it believes today; asking again later returns a different answer for the same past date. Stored rates are a record of what was quoted at the time — preserve them, and use this source to move forward.

URI format ​

plaintext
exchangeratesapi://?access_key=<your-access-key>&base=<currency-code>

URI parameters:

  • access_key (required): your exchangeratesapi.io API access key.
  • base (optional, defaults to EUR): the base currency for the returned rates. Changing the base requires a paid plan. It defaults to the API's own default (EUR) rather than guessing, since a silent base change produces plausible but wrong conversions.

Tables ​

TableGrainStrategyNotes
exchange_ratesone row per (date, base, currency)merge on dateThe main table. Honours --interval-start / --interval-end.
latestone row per (base, currency)mergeMost recent published rates.
symbolsone row per currencyreplaceCurrency codes and names.

Columns for exchange_rates and latest: date, base, currency, exchange_rate. Each day includes a base-to-base identity row with exchange_rate = 1.0, so converting an amount already in the base currency finds a row instead of a NULL.

The base currency can also be given in the table name, which takes precedence over the URI: --source-table 'exchange_rates:CZK'.

Example ​

sh
ingestr ingest \
  --source-uri "exchangeratesapi://?access_key=$EXCHANGERATESAPI_KEY&base=CZK" \
  --source-table 'exchange_rates' \
  --dest-uri "duckdb:///fx.duckdb" \
  --dest-table 'raw.rates' \
  --interval-start 2026-08-07 --interval-end 2026-08-10

One request per day ​

There is no bulk endpoint available on the plans this source was built against. The /timeseries endpoint exists but returns HTTP 403 function_access_restricted on anything below the top tiers, while the single-date endpoint works — so exchange_rates issues one HTTP request per day in the requested interval.

A nightly run asks for one or two days, and a week's catch-up costs seven requests. To stop an accidental --interval-start 2010-01-01 from firing thousands of requests and burning a monthly quota in a single run, intervals longer than 400 days are refused with an explicit error.

Note also that ingestr requires --interval-start to be strictly earlier than --interval-end; a single-day run should ask for the day and the day after.

Errors ​

Failures come back as proper HTTP status codes with an {"error": {...}} body — note there is no success: false field to test, the success key is simply absent. The two you are most likely to meet:

  • invalid_access_key (HTTP 401) — the key is wrong or expired.
  • function_access_restricted (HTTP 403) — the endpoint is above your plan. If you see this on exchange_rates, you are probably on the free tier; use Frankfurter.

The access key is a query parameter on every request, so this source never logs a URL and never builds an error message out of a raw response body.