← 목록

Official exchange rates in Python without an API key (ECB, NBP, CNB, Bank of Canada, Norges Bank)

devto 2026-10-20 원문 보기 ↗


I built this actor; it's a paid tool on Apify with a free trial credit.

Most "free currency API" results want a key, cap your requests, and don't say where their numbers come from. For accounting that last part matters: VAT and bookkeeping rules often name a specific central bank's rate for a specific day. In Poland, for example, you convert a foreign-currency invoice at the NBP average rate from the last business day before the invoice date.

Central banks publish these rates for free, but every bank does it differently: the ECB speaks SDMX, NBP limits requests to 93 days, the CNB serves pipe-separated text with "100 JPY" columns, and Norges Bank has a unit multiplier. I'm an 18-year-old engineering student, and I wrapped five of them in one actor called Central Bank Exchange Rates. This post shows how to call it and one real use: converting invoices at the NBP rate.

How it works, briefly

The actor calls each bank's official open-data endpoint over plain HTTP, parses the rates into "home currency per 1 unit", and re-expresses them in whatever base currency you ask for. Pairs that don't involve the bank's own currency are calculated as cross rates from that bank's same-day fixing and labeled cross rate via EUR (or PLN, CZK, ...), so you always know whether a number is official or derived.

Python example

pip install apify-client
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

run = client.actor("rel8ble/central-bank-exchange-rates").call(run_input={
    "sources": ["ecb"],
    "baseCurrency": "USD",
    "currencies": ["EUR", "GBP", "JPY", "CAD", "CHF"],
})

for r in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(r["date"], r["pair"], r["rate"], r["calculation"])

Leave startDate empty and you get the latest published rates (about 2 seconds per run).

Node example

npm install apify-client
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "<YOUR_APIFY_TOKEN>" });

const run = await client.actor("rel8ble/central-bank-exchange-rates").call({
  sources: ["ecb", "nbp", "cnb", "boc", "norgesbank"],
  baseCurrency: "USD",
  currencies: ["JPY"],
  startDate: "2026-09-24",
  endDate: "2026-09-24",
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const r of items) console.log(r.source, r.pair, r.rate);

What comes back

One row per currency pair, per day, per bank. Real rows from a test run on 24 Sep 2026, trimmed:

[
  {
    "date": "2026-09-24",
    "pair": "USD/EUR",
    "rate": 0.8797396,
    "inverseRate": 1.1367,
    "source": "ecb",
    "calculation": "official",
    "sourcePublishedRate": 1.1367,
    "sourceQuoteConvention": "1 EUR = x USD"
  },
  {
    "date": "2026-09-24",
    "pair": "USD/PLN",
    "rate": 3.857,
    "source": "nbp",
    "calculation": "official",
    "nbpTable": "A 186/A/NBP/2026"
  },
  {
    "date": "2026-09-24",
    "pair": "USD/JPY",
    "rate": 158.83146,
    "source": "boc",
    "calculation": "cross rate via CAD"
  }
]

The Node example above, run for real, gave USD/JPY on that day as 158.48 (NBP), 158.83 (Bank of Canada), 158.85 (CNB), 158.85 (ECB) and 158.86 (Norges Bank). The banks fix at different times of day, so they never match exactly. That's why every row says which bank it came from.

Use case: convert invoices at the NBP rate from the previous business day

Say you have invoices.csv with number,date,currency,amount and need each amount in PLN at the NBP table A rate from the last business day before the invoice date.

import csv
from datetime import date, timedelta
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")

invoices = list(csv.DictReader(open("invoices.csv", encoding="utf-8")))
first = min(date.fromisoformat(i["date"]) for i in invoices) - timedelta(days=10)
last = max(date.fromisoformat(i["date"]) for i in invoices)

run = client.actor("rel8ble/central-bank-exchange-rates").call(run_input={
    "sources": ["nbp"],
    "currencies": sorted({i["currency"] for i in invoices}),
    "startDate": first.isoformat(),
    "endDate": last.isoformat(),
})

# With no baseCurrency, NBP rows are "1 PLN = x CUR"; inverseRate is PLN per 1 unit of CUR.
rates = {}
for r in client.dataset(run["defaultDatasetId"]).iterate_items():
    rates[(r["quote"], r["date"])] = (r["inverseRate"], r["nbpTable"])

def rate_before(cur, d):
    day = d - timedelta(days=1)
    while (cur, day.isoformat()) not in rates:  # skip weekends and holidays
        day -= timedelta(days=1)
    return day, *rates[(cur, day.isoformat())]

for inv in invoices:
    d = date.fromisoformat(inv["date"])
    day, rate, table = rate_before(inv["currency"], d)
    pln = round(float(inv["amount"]) * rate, 2)
    print(f'{inv["number"]}: {inv["amount"]} {inv["currency"]} x {rate} ({table}, {day}) = {pln} PLN')

The 10-day buffer before the first invoice covers long weekends. The table number goes straight onto the invoice, which Polish rules also ask for. The same pattern works for the ECB (euro-area VAT) or the CNB (Czech accounting): change sources.

What it costs

Pricing is $0.50 per 1,000 results, and one result is one currency pair on one day from one bank.

Filter with currencies so you only pay for the pairs you use.

Limits

These come straight from my testing:

If something breaks or a field is wrong, the Issues tab on the actor page reaches me directly.

Central Bank Exchange Rates on Apify

This article was drafted with AI and published by me.