# Currency Exchange Rates Scraper - ECB Reference Rates & History (`neverempty/currency-exchange-rates-scraper`) Actor

Official ECB euro foreign exchange reference rates for about 30 currencies since 1999: the latest day or any date range, against EUR or any base currency (cross rates computed via EUR and marked as computed). One row per day and currency with the ECB series key. No API key.

- **URL**: https://apify.com/neverempty/currency-exchange-rates-scraper.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Developer tools, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.84 / 1,000 exchange rate returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Currency Exchange Rates Scraper - ECB Reference Rates & History

Get the **official euro foreign exchange reference rates of the European Central Bank (ECB)** as a clean table: the latest day for every currency, or the daily history of any currencies since 1999, against the euro or against **any base currency** (USD, GBP, JPY...). One row per day and currency. No API key, no account, no proxy.

The data comes from the ECB's own statistical API (`data-api.ecb.europa.eu`, dataset EXR, series `D.<currency>.EUR.SP00.A`). The ECB sets these rates at 14:15 CET on every TARGET business day and publishes them around 16:00 CET.

### What you get

| Column | Meaning |
|---|---|
| `date` | The ECB reference day (YYYY-MM-DD). |
| `baseCurrency`, `quoteCurrency` | The pair: **1 base = rate x quote**. Names in `baseCurrencyName`, `quoteCurrencyName`. |
| `rate` | Units of the quote currency per 1 unit of the base currency. With base EUR this is **the ECB value itself, unrounded**. |
| `rateIsComputed` | `false` when `rate` is the ECB's published value; `true` when it is a cross rate calculated from ECB values (any base other than EUR). |
| `inverseRate` | Units of the base currency per 1 unit of the quote currency. |
| `inverseRateIsComputed` | `true` when `inverseRate` is calculated (1 / rate, 10 significant digits) - which is every row except one kind: with a base other than EUR, the row whose quote is EUR, where `inverseRate` is the ECB value itself (`false`). |
| `rateText` | The rate in words, for example `1 EUR = 1.1592 USD`. |
| `calculation` | For computed rates, the exact formula and the ECB numbers used, for example `(JPY per EUR) / (USD per EUR) = 185.63 / 1.159`. |
| `ecbSeriesKey`, `ecbQuotePerEur` | The ECB series of the quote currency and its published value for that day. |
| `ecbBaseSeriesKey`, `ecbBasePerEur` | The ECB series of the base currency and its published value (only when the base is not EUR). |
| `source` | `Source: ECB statistics.` - the attribution the ECB asks for. |
| `status`, `note`, `input`, `checkedAt` | `ok` for a rate row. Any other status is an explanation row (see below). `input` records what was asked. |

Example rows (real output):

```json
{ "date": "2026-09-03", "baseCurrency": "USD", "quoteCurrency": "JPY", "rate": 156.0137753, "rateIsComputed": true,
  "inverseRate": 0.006409690414, "inverseRateIsComputed": true, "rateText": "1 USD = 156.0137753 JPY",
  "calculation": "(JPY per EUR) / (USD per EUR) = 181.21 / 1.1615",
  "ecbSeriesKey": "EXR.D.JPY.EUR.SP00.A", "ecbQuotePerEur": 181.21, "ecbBaseSeriesKey": "EXR.D.USD.EUR.SP00.A", "ecbBasePerEur": 1.1615 }
{ "date": "2026-09-11", "baseCurrency": "EUR", "quoteCurrency": "USD", "rate": 1.1592, "rateIsComputed": false,
  "inverseRate": 0.8626639061, "inverseRateIsComputed": true, "rateText": "1 EUR = 1.1592 USD", "calculation": null,
  "ecbSeriesKey": "EXR.D.USD.EUR.SP00.A", "ecbQuotePerEur": 1.1592 }
```

### Input

| Field | What it does |
|---|---|
| `currencies` | Three-letter ISO 4217 codes, for example `["USD", "JPY", "GBP", "CHF"]`. **Leave it empty to get every currency the ECB publishes** (29 on 2026-09-11; older ones such as HRK or RUB appear on the dates they were published). |
| `baseCurrency` | Default `EUR`. Any other code (for example `USD`) returns cross rates computed from the two ECB euro rates of the same day, marked `rateIsComputed: true`. `EUR` can then be one of the `currencies`. |
| `startDate`, `endDate` | A date range (YYYY-MM-DD, both days included). Without `endDate` the range runs to the latest published day. The series start on 1999-01-04. |
| `latestDays` | Used when no dates are given: the latest N ECB reference days (default 1 = the latest published rates). 5 usually covers one working week. |
| `maxResults` | The most rate rows per run. Leave it empty for the default: **1,000 when `currencies` is empty, 10,000 when you name currencies**. Up to 300,000. |

**How many rows (and what it costs).** Rows = currencies x ECB reference days (about 255 a year). All currencies are about 30 rows per reference day, so **all currencies over one year is about 7,500 rows** (about $60 at $8 per 1,000 rows), and the default of 1,000 rows covers about the last 34 reference days. One currency's full history since 1999 is about 7,100 rows.

**Order.** With `currencies` empty, rows come **newest date first, every currency of a day together** (alphabetical within the day; with another base, EUR first). When `maxResults` or your spending limit stops the run, you have complete recent days and only the oldest dates are missing; an explanation row names the dates that were not returned. With named currencies, rows come currency by currency in the order you gave, oldest date first.

Examples:

```json
{}
```

The latest ECB rates of all currencies against the euro (one row each, about 30 rows).

```json
{ "currencies": ["USD", "GBP", "JPY"], "startDate": "2025-01-01", "endDate": "2025-12-31" }
```

Every 2025 reference day for three currencies (765 rows).

```json
{ "baseCurrency": "USD", "currencies": ["EUR", "JPY", "GBP", "CNY"], "latestDays": 5 }
```

The last five reference days quoted per US dollar (20 rows).

### Weekends and holidays have no rows

The ECB publishes reference rates **only on TARGET business days**: never on Saturdays or Sundays, and not on TARGET holidays (1 January, Good Friday, Easter Monday, 1 May, 25 and 26 December). This actor **does not invent rows for those days** and does not carry the previous day forward: a week gives 5 rows per currency, a range over Christmas gives fewer. If you need a value for every calendar day, fill the gaps yourself with the rule your accounting uses (most use the last available rate).

A range that contains no reference day at all (a weekend, or dates before today's publication) returns an explanation row instead of an empty dataset.

The ECB's API also lists some days with an empty value (for the US dollar 62 days between 1999 and 2012, 13 of them in 1999-2001, for example 1999-12-31 and 2000-12-25). Those are not rates: they are left out, never turned into zero, and the full history matches the ECB's own history file day for day (7,091 US dollar rows on 2026-09-11).

### Cross rates are computed, and say so

The ECB publishes rates **against the euro only**. For any other base currency this actor divides the two ECB euro rates of the same day: `USD -> JPY = (JPY per EUR) / (USD per EUR)`. Those rows carry `rateIsComputed: true`, the formula in `calculation`, and both ECB values in `ecbQuotePerEur` and `ecbBasePerEur`, so you can always tell an ECB figure from a derived one. Computed values have 10 significant digits; the ECB values they come from have about 5, so a cross rate is not more precise than that. A day on which the ECB has a rate for the quote currency but not for the base currency gets no row (it is counted in an explanation row, never filled with zero).

### Turkish lira and Romanian leu before 2005

The ECB has no separate series for the old Turkish lira (TRL) or the old Romanian leu (ROL). Their rates are in the **TRY and RON series, converted to the new units**: TRY rows up to 2004-12-31 are old lira divided by 1,000,000, and RON rows up to 2005-06-30 are old lei divided by 10,000 (TRY on 1999-01-04 is 0.372274, where the ECB's history file lists 372274 old lira). Multiply by 1,000,000 or 10,000 to get the old units. Asking for TRL or ROL returns an explanation row that says this.

### Explanation rows (never charged)

Every row whose `status` is not `ok` explains something and is **not charged**:

- `bad-input` - a code that is not three letters, a date that does not exist, a start date after today, a start after the end, or a currency equal to the base. Nothing is sent to the ECB for it.
- `currency-not-found` - the ECB has no reference rate series for that code (it answered "no such series"). For TRL and ROL the row points to TRY and RON.
- `no-rates-in-range` - the currency exists but has no rate in your dates, and the row says why: the ECB stopped publishing it (for example RUB after 2022-03-01), its series starts later, or the range holds no reference day.
- `incomplete` / `not-looked-up` - the run stopped at `maxResults` or at the spending limit you set, and the row names what was not returned.
- `failed` - the ECB did not answer after several retries. Nothing is guessed; running again later usually works.
- `refused` - the ECB refused the query (HTTP 400). It is not retried, because the same query gets the same answer.

When some lookups failed, the run's status message says how many. When **every** lookup failed, the run ends as failed with the first problem in its status message, so a scheduled run or an integration can notice.

### How accurate is it

Checked on 2026-09-12 against the ECB's other official publication of the same rates, the full-history file `eurofxref-hist.zip`: named currencies over six weeks, a range across the 2025-26 new year holidays, September-October 2008, cross rates on a USD base, HRK and RUB in 2022, TRY and RON across their 2005 change of unit, all currencies over three months, the default input and the full US dollar history since 1999 matched in **number of days, dates and values: 0 differences**. The check script is kept with the source so it can be re-run.

### Currencies

Currently published (2026-09-11): AUD, BRL, CAD, CHF, CNY, CZK, DKK, GBP, HKD, HUF, IDR, ILS, INR, ISK, JPY, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, SEK, SGD, THB, TRY, USD, ZAR.

Discontinued, history available up to the date shown: RUB (2022-03-01), HRK (2022-12-30), BGN (2025-12-31), ARS, DZD, MAD, TWD (2020-10-30), and the former national currencies of euro area members (CYP, EEK, GRD, LTL, LVL, MTL, SIT, SKK).

### Data source and terms

Source: ECB statistics. The ECB's policy on reusing ESCB statistics (https://www.ecb.europa.eu/stats/ecb\_statistics/governance\_and\_quality\_framework/html/usage\_policy.en.html) allows free reuse, including commercial use, **on the condition that the source is quoted and the statistics are not modified**. This actor passes ECB values through unchanged (`ecbQuotePerEur`, `ecbBasePerEur`, and `rate` when the base is EUR), and every value it calculates is marked (`rateIsComputed`, `inverseRateIsComputed`). Each row carries the source line in `source`. Keep it when you republish the data. The ECB notes that its statistics may be revised.

The ECB reference rates are published for information purposes only; they are not the rates at which you can trade.

### Pricing

You pay per rate row returned. Explanation rows are free. The default input (all currencies, latest day) returns about 30 rows. See "How many rows" above before asking for all currencies over a long period.

# Actor input Schema

## `currencies` (type: `array`):

Three-letter ISO 4217 codes, for example USD, JPY, GBP, CHF. Leave empty to get every currency the ECB publishes a reference rate for (about 30 today; older ones such as RUB or HRK when your dates cover them). A code the ECB does not publish is reported in its own row, which is not charged.

## `baseCurrency` (type: `string`):

The currency each rate is quoted against: 1 base = rate x quote. The ECB publishes rates against the euro only, so for any other base (for example USD) every rate is a cross rate computed from the two ECB euro rates of the same day; those rows say rateIsComputed = true and show the calculation and both ECB values.

## `startDate` (type: `string`):

YYYY-MM-DD. Returns every ECB reference day from this date on (the series start on 1999-01-04). Leave empty, together with To date, to get only the latest reference days.

## `endDate` (type: `string`):

YYYY-MM-DD, included. Leave empty to go up to the latest published day. Needs From date.

## `latestDays` (type: `integer`):

Used when no dates are given: how many of the most recent ECB reference days to return per currency. 1 gives the latest published rates. Weekends and TARGET holidays have no rates, so 5 days usually cover one working week.

## `maxResults` (type: `integer`):

The most rate rows one run returns. Leave empty for the default: 1,000 when Currencies is empty (all currencies), 10,000 when you name currencies. Rows = currencies x ECB reference days: all currencies over one year is about 7,500 rows, one currency since 1999 about 7,100. With all currencies the newest dates come first, so a limit only drops the oldest dates; a row says which dates were not returned.

## Actor input object example

```json
{
  "currencies": [
    "USD",
    "JPY",
    "GBP",
    "CHF"
  ],
  "baseCurrency": "EUR",
  "latestDays": 1
}
```

# Actor output Schema

## `results` (type: `string`):

One row per ECB reference day and currency: date, base and quote currency, rate and inverse rate, whether the rate was computed (cross rate) or is the ECB value itself, the ECB series key and the ECB euro values used, plus one row when an input was rejected, a currency has no ECB rate, or something could not be read.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "currencies": [
        "USD",
        "JPY",
        "GBP",
        "CHF"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/currency-exchange-rates-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "currencies": [
        "USD",
        "JPY",
        "GBP",
        "CHF",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/currency-exchange-rates-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "currencies": [
    "USD",
    "JPY",
    "GBP",
    "CHF"
  ]
}' |
apify call neverempty/currency-exchange-rates-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/currency-exchange-rates-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QxnYCST7EGZy1ahPb/builds/EKADoibhVXfI78TXl/openapi.json
