DEV Community

Fast Data
Fast Data

Posted on

Your invoice is dated on a Saturday. Which ECB exchange rate do you use? (Python)

Here is a USD 1,250 invoice dated Saturday, 7 November 2026. What is it worth in euros?

The ECB publishes no reference rate on Saturdays. So your code either crashes, silently uses some other day, or asks a human. And the day matters more than people expect: between 2 and 10 November, the same USD 1,250 was worth between €1,109.43 and €1,119.57 depending on which daily ECB rate you pick.

EUR value of a USD 1,250 invoice per ECB rate day, 2 to 10 November 2026

That is up to €10.14 on one mid-sized invoice, from rate-day choice alone, and even the two neighbouring fixing days (Friday vs Monday) differ by €2.80. Across a few hundred invoices a month, "we just used the rate from somewhere" gets hard to reconcile for whoever closes the books.

Disclosure: I built the ECB Exchange Rates – Invoice Currency Converter on Apify, which the code below uses. The rules in this post apply to any implementation.

The four decisions behind every conversion

Decision Options Actor default (your policy may differ)
Which rate source? ECB reference rate, bank rate, a "mid-market" API ECB reference rate (public, reproducible)
Which day? invoice date, delivery date, payment date invoice date
No rate that day? previous fixing day, next fixing day, fail previous fixing day, flagged
Which rounding? per line, per invoice, half-up, banker's half-up to 2 decimals, keep the unrounded value

The third row is where homemade scripts often go wrong. The ECB publishes its euro reference rates for about 30 currencies on TARGET working days, around 16:00 CET. No rate on weekends, none on TARGET holidays such as 1 January or Good Friday, and none for today until the afternoon.

Converting a batch of invoices

import os
import requests

URL = ("https://api.apify.com/v2/acts/fast_data_asap~ecb-exchange-rates-invoice-converter"
       "/run-sync-get-dataset-items")

invoices = [
    {"id": "INV-1001", "amount": 1250.00, "currency": "USD", "date": "2026-11-07"},
    {"id": "INV-1002", "amount": 480.50,  "currency": "GBP", "date": "2026-11-05"},
    {"id": "INV-1003", "amount": 2999.90, "currency": "CHF", "date": "2026-10-30"},
]

HEADERS = {"Authorization": f"Bearer {os.environ['APIFY_TOKEN']}"}

resp = requests.post(
    URL,
    headers=HEADERS,
    json={"mode": "convert", "items": invoices, "targetCurrency": "EUR", "fallback": "previous"},
    timeout=120,
)
resp.raise_for_status()

for row in resp.json():
    if row["status"] != "ok":
        print(row["id"], "ERROR:", row["error"])
        continue
    flag = f"  <- rate of {row['rateDate']}" if row["fallbackUsed"] else ""
    print(f"{row['id']}: {row['amount']} {row['currency']} = {row['convertedAmount']} {row['targetCurrency']} "
          f"(rate {row['rate']}){flag}")
Enter fullscreen mode Exit fullscreen mode

Real output of that input:

INV-1001: 1250 USD = 1119.57 EUR (rate 1.1165)  <- rate of 2026-11-06
INV-1002: 480.5 GBP = 576.19 EUR (rate 0.83393)
INV-1003: 2999.9 CHF = 3262.89 EUR (rate 0.9194)
Enter fullscreen mode Exit fullscreen mode

Each row also carries daysShifted (here -1), convertedAmountUnrounded (1119.57008509) and a sourceUrl that returns exactly the ECB observation that was used:

https://data-api.ecb.europa.eu/service/data/EXR/D.USD.EUR.SP00.A?format=csvdata&startPeriod=2026-11-06&endPeriod=2026-11-06
Enter fullscreen mode Exit fullscreen mode

That URL is the audit trail. Store it next to the booking and anyone can check the number later without trusting your code.

What the three fallback rules give you

Same invoice, USD 1,250 on Saturday 7 November, three settings:

fallback Rate day used Rate EUR
previous Fri 6 Nov 1.1165 1,119.57
next Mon 9 Nov 1.1193 1,116.77
none – – error row, so a human decides

previous is the actor's default because that rate was already published on the invoice date. next can make sense if your policy is "first rate published after the document". none is right when you would rather stop than guess. The bad option is the one many scripts implement by accident: no rule at all.

Three gotchas I hit while building it

1. The ECB quotes "units of foreign currency per 1 EUR". 1.1165 means 1 EUR = 1.1165 USD, so USD → EUR is a division: 1250 / 1.1165. Multiplying gives you €1,395.63 and a very confused accountant.

2. Cross rates go through the euro. There is no ECB USD/GBP rate. USD → GBP is amount / rate(USD) * rate(GBP), with both rates from the same fixing day. Mix a cached rate for one currency with a fresh one for the other and your cross rate quietly comes from two different days.

3. Fetch per currency, not per invoice. One request per invoice is slow and impolite to a free public API. Group by currency, request the whole date range once, then look up each invoice locally. The actor sends one ECB data request per currency per run (plus retries on errors), however many invoices you pass.

If you only need the rates

The rates mode returns the daily series instead:

resp = requests.post(URL, headers=HEADERS, timeout=120,
                     json={"mode": "rates", "currencies": ["USD", "JPY"],
                           "startDate": "2026-11-02", "endDate": "2026-11-10"})
resp.raise_for_status()
for row in resp.json():
    print(row["date"], row["currency"], row["rate"], row["status"])
Enter fullscreen mode Exit fullscreen mode

For 2 to 10 November that is 7 fixing days per currency (the weekend is simply missing), for example USD 1.1216 on 2 November and 1.1214 on 10 November. An unknown code such as XYZ comes back as one error row instead of crashing the run.

What this is not

  • Not tax advice. The ECB itself says its reference rates are for information purposes only. For VAT, some countries prescribe other rates (in Germany, for example, the Federal Ministry of Finance publishes monthly average rates, based on the ECB rates, under § 16 (6) UStG). Ask whoever signs your tax return which rate applies.
  • Not affiliated with the ECB. The actor only reads the ECB's public Data Portal API.
  • Not a trading rate. Your bank converted the actual payment at its own rate. The difference is usually booked separately as an FX gain or loss; your accountant decides how.

Try it

ECB Exchange Rates – Invoice Currency Converter on Apify: $0.001 per result row (converted amount or daily rate), error rows are free, no API key for the ECB needed. It pairs with our XRechnung & ZUGFeRD parser: take currency, grandTotal and issueDate from a parsed e-invoice and convert it here. AI agents can call it as an MCP tool: https://mcp.apify.com?tools=fast_data_asap/ecb-exchange-rates-invoice-converter.

Question: which rate day does your team use for foreign-currency invoices: invoice date, delivery date or payment date? And has an auditor ever asked you to prove where a rate came from?

Top comments (0)