# Historical Exchange Rates & Currency Converter (`cliqtomedia/historical-fx-rates-converter`) Actor

Official ECB historical exchange rates for any currency pair and date: exact cross rates, weekend and holiday handling with clear notes, optional last-published fill and amount conversion. 30+ currencies since 1999, via the Frankfurter API.

- **URL**: https://apify.com/cliqtomedia/historical-fx-rates-converter.md
- **Developed by:** [Cliqto Media](https://apify.com/cliqtomedia) (community)
- **Categories:** Business, Developer tools
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.75 / 1,000 exchange rate rows

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Get official European Central Bank (ECB) exchange rates for any currency pair and any date since 1999, and convert amounts with them.
Add a pair like `USD/JPY` and a date, and you get one clear row per pair and day. Weekends, ECB holidays and days not published yet are marked, not hidden. No API key is needed, and a typical run (2 pairs, last 7 days) costs about $0.01.

![Historical Exchange Rates: add pairs, dates and amounts, get the exact ECB rate and converted amount, and see why a day has no rate](https://api.apify.com/v2/key-value-stores/0Ikph5KJI0tJrsiV7/records/README-BANNER-20261012.png)

### Table of contents

- [What it does](#what-it-does)
- [Quick start](#quick-start)
- [Input](#input)
- [Output](#output)
- [Pricing and limits](#pricing-and-limits)
- [API, export and integrations](#api-export-and-integrations)
- [Troubleshooting](#troubleshooting)
- [FAQ](#faq)
- [Data source and licence](#data-source-and-licence)
- [Support](#support)

### What it does

The Actor reads the **euro foreign exchange reference rates** that the ECB publishes every business day at about 16:00 CET. It returns one dataset row for each **pair × date** (and for each amount, if you add amounts).

- **Any pair.** The ECB publishes rates against the euro. For pairs without the euro, like `USD/JPY`, the Actor computes the cross rate from the two euro rates and shows both of them (`eurToBase`, `eurToQuote`). You can check the math yourself.
- **Any date since 1999-01-04.** You can use a list of dates, a date range or the last N days.
- **Every day explained.** A day without a rate has `status: no_rate` and a reason in `note`: `weekend`, `ecb_holiday`, `not_yet_published` or `currency_not_quoted`.
- **Optional fill.** With `missingDatePolicy: last_published`, a missing day gets the last published rate before it. `effectiveDate` always shows which day's rate was used.
- **Conversion.** Add amounts and get `convertedAmount` = amount × rate, rounded the way you choose.

#### Good fit

- Accounting and invoices: the official ECB rate for the invoice date.
- Finance and audit: a full rate history for a pair, with holidays clearly marked.
- Reports and BI: a clean daily table of rates for several pairs.

#### Not a fit / limits

- No live or intraday rates. The ECB publishes once a day; today's rate appears after about 16:00 CET.
- About 30 currencies are quoted today (45 in the full history). No crypto, no exotic currencies outside the ECB list.
- These are reference rates for information. They are not bank buy/sell rates.

### Quick start

1. Open the **Input** tab.
2. In **Currency pairs** (`pairs`), keep the prefill `EUR/USD` and `EUR/GBP` or add your own, for example `USD/JPY`.
3. Keep **Last days** (`lastDays`) at 7, or add **Specific dates** (`dates`), for example `2024-12-31`.
4. Click **Start**.
5. Open the **Output** tab. Each row has `pair`, `requestedDate`, `status`, `rate` and `note`.

Expected result: one row per pair and day. Business days have `status: ok` and a `rate`. Weekends have `status: no_rate` and `note: weekend`. A 7-day run finishes in a few seconds.

### Input

#### Input fields

| Field | Plain name | Type | Required | Default / prefill | Allowed values | Effect |
| --- | --- | --- | --- | --- | --- | --- |
| `pairs` | Currency pairs | list of text | yes | prefill `EUR/USD`, `EUR/GBP` | 1–10 pairs as `BASE/QUOTE`; `EURUSD` and `eur-usd` also work | Rate = how many QUOTE units one BASE unit buys |
| `dates` | Specific dates | list of text | no | — | 1–100 dates `YYYY-MM-DD`, 1999-01-04 to today (UTC) | One row per date, in your order |
| `dateFrom` | Date from | date | no | — | `YYYY-MM-DD` | Start of a range; use with `dateTo` |
| `dateTo` | Date to | date | no | — | `YYYY-MM-DD`, not after today | End of a range; at most 366 days |
| `lastDays` | Last days | number | no | 7 (prefill 7) | 1–366 | Every day from N−1 days ago to today (UTC) |
| `provider` | Rate source | select | no | `ECB` | `ECB` | Official source of the rates |
| `missingDatePolicy` | Days without a rate | select | no | `none` | `none`, `last_published`, `skip` | What to do on weekends, holidays and days not published yet |
| `amounts` | Amounts to convert | list of numbers | no | — | 0–10 amounts in the BASE currency | One row per amount with `convertedAmount` |
| `rounding` | Decimal places | number | no | 2 | 0–8 | Rounding of `convertedAmount` (half away from zero) |

- Duplicate pairs, dates and amounts are removed. The run summary shows a warning.
- The run is limited to 10,000 rows (pairs × dates × amounts). A bigger input stops before any request with `INVALID_INPUT`.
- An unknown currency code (for example `XYZ`) does not stop the run. Its rows have `status: unsupported_currency`.

#### Interacting settings

| When | Effective behavior |
| --- | --- |
| `dates` is set | `dates` is used. `dateFrom`, `dateTo` and `lastDays` are ignored (with a warning). |
| `dateFrom` and `dateTo` are set, `dates` is empty | The range is used. `lastDays` is ignored. Both range fields are needed. |
| Nothing is set | `lastDays` is used (7 if empty). |
| `missingDatePolicy: none` | Days without a rate get a `no_rate` row with the reason. |
| `missingDatePolicy: last_published` | Days without a publication get the last rate from up to 10 days before, with `note: last_published_used`. A currency the ECB stopped quoting is never carried forward. |
| `missingDatePolicy: skip` | Days without a rate are not written. The run summary counts them. |

#### Input examples

##### Convert invoice amounts on fixed dates

```json
{
  "pairs": ["USD/JPY", "GBP/EUR"],
  "dates": ["2024-12-31", "2025-12-25"],
  "missingDatePolicy": "last_published",
  "amounts": [1000, 2500.5]
}
```

Expected behavior: 2 pairs × 2 dates × 2 amounts = 8 rows. 2025-12-25 is an ECB holiday, so those rows use the rate of 2025-12-24.
Expected output: `rate`, `effectiveDate`, `convertedAmount`. For example, 1,000 USD on 2024-12-31 = 156,954.47 JPY.

##### Rate history for one year

```json
{
  "pairs": ["EUR/USD", "EUR/CHF"],
  "dateFrom": "2025-01-01",
  "dateTo": "2025-12-31"
}
```

Expected behavior: 2 × 365 = 730 rows. Business days have a rate; weekends and holidays have `no_rate` with a reason. Add `"missingDatePolicy": "skip"` to get only the days with a rate.

### Output

Each dataset row is one pair, one requested date and (optionally) one amount.

#### Output example

From the first input example above (one of 8 rows, full):

```json
{
  "pair": "USD/JPY",
  "base": "USD",
  "quote": "JPY",
  "requestedDate": "2024-12-31",
  "effectiveDate": "2024-12-31",
  "status": "ok",
  "note": null,
  "noteText": null,
  "rate": 156.9544711,
  "inverseRate": 0.006371274377,
  "rateType": "cross_via_EUR",
  "eurToBase": 1.0389,
  "eurToQuote": 163.06,
  "amount": 1000,
  "convertedAmount": 156954.47,
  "provider": "ECB",
  "providerName": "European Central Bank",
  "providerLastPublished": "2026-10-09",
  "source": "Frankfurter API v2 (frankfurter.dev)",
  "sourceUrl": "https://api.frankfurter.dev/v2/rates?base=EUR&quotes=GBP%2CJPY%2CUSD&providers=ECB&from=2024-12-21&to=2024-12-31",
  "fetchedAt": "2026-10-12T10:48:11.221Z"
}
```

The holiday row for the same pair has `requestedDate: 2025-12-25`, `effectiveDate: 2025-12-24`, `rate: 155.9599559` and `note: last_published_used`. `noteText` explains the reason: “No publication on this business day (TARGET/ECB holiday)”.

#### Output fields

| Field | Meaning | Can be empty? | Example / values |
| --- | --- | --- | --- |
| `pair`, `base`, `quote` | The pair as `BASE/QUOTE` and its two codes | no | `USD/JPY` |
| `requestedDate` | The date you asked for | no | `2025-12-25` |
| `effectiveDate` | The ECB publication date of the rate | when there is no rate | `2025-12-24` |
| `status` | Result of the row | no | `ok`, `no_rate`, `unsupported_currency` |
| `note` | Why the row is special | yes (normal day) | `weekend`, `ecb_holiday`, `not_yet_published`, `currency_not_quoted`, `last_published_used`, `same_currency`, `unsupported_currency` |
| `noteText` | The note in plain English | yes | — |
| `rate` | QUOTE units for 1 BASE unit | when there is no rate | `156.9544711` |
| `inverseRate` | 1 / rate | when there is no rate | `0.006371274377` |
| `rateType` | How the rate was made | when there is no rate | `direct` (EUR/X), `inverse` (X/EUR), `cross_via_EUR`, `identity` |
| `eurToBase`, `eurToQuote` | The ECB euro rates used | when there is no rate | `1.0389`, `163.06` |
| `amount` | Your amount in BASE | yes (no amounts) | `1000` |
| `convertedAmount` | amount × rate, rounded | yes | `156954.47` |
| `provider`, `providerName` | Rate source | no | `ECB`, `European Central Bank` |
| `providerLastPublished` | Latest ECB publication date at run time | no | `2026-10-09` |
| `source`, `sourceUrl` | Where the numbers came from | no | — |
| `fetchedAt` | When the Actor read the source | no | ISO time |

Direct rates are exactly the ECB numbers. Cross and inverse rates keep 10 significant digits.

#### Output surfaces

| Situation | Where | What you see |
| --- | --- | --- |
| Normal run | Dataset | Rows with `status: ok`; run summary `COMPLETE` |
| Only weekends or holidays | Dataset | `no_rate` rows with the reason; summary `NO_RESULTS` |
| Some codes unknown | Dataset | `unsupported_currency` rows; summary `PARTIAL` |
| Source problem | Key-value store `RUN_SUMMARY` | No rows for the failed dates; `sourceError` lists them; summary `PARTIAL` or `ERROR` |
| Bad input | Key-value store `RUN_SUMMARY` | `INVALID_INPUT` with the reason; the run fails, no rows |

`RUN_SUMMARY` also has counts (`ok`, `noRate`, `lastPublishedUsed`, …), warnings and the ECB source links.

#### Moving from another FX Actor

If you used an Actor with snake_case fields, map them like this:

| Old field | This Actor |
| --- | --- |
| `base_currency` | `base` |
| `target_currency` | `quote` |
| `rate` | `rate` |
| `inverse_rate` | `inverseRate` |
| `date` | `effectiveDate` (the rate's day) or `requestedDate` (your day) |
| `extracted_at` | `fetchedAt` |

### Pricing and limits

#### Pricing unit

This Actor uses **pay per event**. You pay only for these events:

| Event | When | Price |
| --- | --- | --- |
| Actor start | Once per run | $0.002 |
| Rate row | Each row with `status: ok` and a real rate | $0.0009 (Free plan) · $0.00085 (Bronze) · $0.0008 (Silver) · $0.00075 (Gold and higher) |
| Note row | Each `no_rate`, `unsupported_currency` or same-currency row | $0.00003 |

- Platform usage (compute, storage) is included. You do not pay it.
- Rows from `missingDatePolicy: skip` are not written and not charged.
- If the source fails, no rows are written for the failed dates, so they are not charged.
- An invalid input costs only the Actor start.
- If you set a maximum cost for the run, the Actor stops writing rows before it goes over it. The summary says `CAPPED`.

The ECB rates are free at the source. You pay for the processing: cross rates, holiday checks, conversion and a clean table.

#### Cost examples (Free plan prices)

| Scenario | Rows | Cost |
| --- | --- | ---: |
| 10 rate rows | e.g. 1 pair × 10 business days | $0.011 |
| Prefill run: 2 pairs × last 7 days | 8 rate rows + 6 note rows | $0.0094 |
| 100 rate rows | e.g. 1 pair × 100 dates | $0.092 |
| 1,000 rate rows | e.g. 4 pairs × 1 year with `skip` | $0.902 |
| 1 year, 1 pair, every day | about 255 rate rows + 110 note rows | about $0.234 |

#### Limits

- Up to 10 pairs, 100 dates or a 366-day range, 10 amounts and 10,000 rows per run.
- The largest tested run (10,000 rows) took about 30 seconds.
- Dates from 1999-01-04 to today (UTC).

### API, export and integrations

- **API:** start the Actor with the same JSON input and read the dataset by API.
- **Export:** download the dataset as JSON, CSV, Excel or XML from the Output tab.
- **Scheduling:** run it every day after 16:30 CET with `lastDays: 1` to save the new rate.

#### Use with AI agents (MCP)

- MCP server with only this Actor: `https://mcp.apify.com?tools=cliqtomedia/historical-fx-rates-converter`
- Smallest useful input: `{"pairs": ["EUR/USD"], "dates": ["2024-12-31"]}`
- Key output fields: `rate` (QUOTE per 1 BASE), `effectiveDate` (the ECB day used), `status` (`ok` / `no_rate` / `unsupported_currency`), `note` (reason for a missing rate), `convertedAmount`.
- A small run finishes in a few seconds. Cost: $0.002 per run plus $0.00075–$0.0009 per rate row and $0.00003 per note row. Limits: 10 pairs, 100 dates or 366 days, 10 amounts, 10,000 rows.

### Troubleshooting

#### Today's row is `not_yet_published`

The ECB publishes at about 16:00 CET. Run again later, or use `missingDatePolicy: last_published` to get the previous rate.

#### A currency has `currency_not_quoted`

The ECB published rates that day, but not for this currency. It was not quoted yet (for example CNY before 2005) or it was suspended or removed (for example RUB after 2022-03-01). The Actor does not carry such rates forward.

#### Rows are missing and the summary has `sourceError`

The rate source did not answer for some dates. `sourceError.failedDates` lists them, and they were not charged. Run again for those dates later.

#### The run failed with `INVALID_INPUT`

Read the message in `RUN_SUMMARY`. Common reasons: a date before 1999-01-04 or in the future, a wrong date format, or more than 10,000 rows.

### FAQ

#### Do I need an API key?

No. The Actor reads public ECB data.

#### Why are there no rates on weekends?

The ECB does not publish on weekends and on TARGET holidays (for example 1 January, Good Friday, Easter Monday, 1 May, 25–26 December). These days get a `no_rate` row with the reason, or the previous rate if you choose `last_published`.

#### Are these the same as bank rates?

No. ECB reference rates are for information. Banks and card companies use their own buy and sell rates.

#### How fresh is the data?

The Actor reads the source on every run. `providerLastPublished` shows the latest ECB publication at that time.

### Data source and licence

Source: ECB statistics (euro foreign exchange reference rates), read through the open [Frankfurter API](https://frankfurter.dev). Cross rates are computed by this Actor from the ECB euro rates. The ECB allows reuse of its statistics with the source named. See the [ECB reference rates page](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html). This Actor is not made by or linked to the ECB.

### Support

For bugs, questions or feature requests, open an issue in the **Issues** tab. Include the run ID and the input. Do not send private data.

# Changelog

This Actor's version history is a separate document: https://apify.com/cliqtomedia/historical-fx-rates-converter/changelog.md

# Actor input Schema

## `pairs` (type: `array`):

Up to 10 pairs as BASE/QUOTE with 3-letter ISO codes, e.g. EUR/USD, USD/JPY or GBP/PLN. Also accepted: EURUSD, eur-usd. The rate is how many QUOTE units one BASE unit buys. Any two ECB currencies can be combined; pairs without EUR are computed exactly from the two ECB euro rates. Codes the ECB does not publish get an unsupported_currency row.

## `dates` (type: `array`):

Up to 100 dates in YYYY-MM-DD format, e.g. 2024-12-31, from 1999-01-04 to today (UTC). Rows keep this order. When set, Date from/to and Last days are ignored.

## `dateFrom` (type: `string`):

Start of a date range (YYYY-MM-DD), used together with Date to. Every calendar day in the range gets a row, at most 366 days. Ignored when Specific dates is set.

## `dateTo` (type: `string`):

End of the date range (YYYY-MM-DD), not later than today (UTC).

## `lastDays` (type: `integer`):

Used only when no dates and no range are given: every calendar day from N−1 days ago to today (UTC). Default 7. Today usually shows not_yet_published until the ECB releases rates around 16:00 CET.

## `provider` (type: `string`):

Official source of the rates. ECB reference rates are published on TARGET business days around 16:00 CET for about 30 currencies (more historically).

## `missingDatePolicy` (type: `string`):

What to do on weekends, ECB holidays and days not published yet. last_published looks back at most 10 days and always shows effectiveDate; it never carries forward a currency the ECB stopped quoting. Default: none.

## `amounts` (type: `array`):

Optional, up to 10 amounts in the BASE currency, e.g. 100 or 2500.50. Each amount gets its own row with convertedAmount = amount × rate. Leave empty to get rates only.

## `rounding` (type: `integer`):

Decimal places for convertedAmount (0–8), rounded half away from zero. Rates are never rounded beyond what the ECB publishes (cross rates keep 10 significant digits). Default 2.

## Actor input object example

```json
{
  "pairs": [
    "EUR/USD",
    "EUR/GBP"
  ],
  "lastDays": 7,
  "provider": "ECB",
  "missingDatePolicy": "none",
  "rounding": 2
}
```

# Actor output Schema

## `rates` (type: `string`):

Rate rows as a table.

## `runSummary` (type: `string`):

Status, counts, provider last publication, source attribution and request statistics.

# 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 = {
    "pairs": [
        "EUR/USD",
        "EUR/GBP"
    ],
    "lastDays": 7,
    "provider": "ECB",
    "missingDatePolicy": "none"
};

// Run the Actor and wait for it to finish
const run = await client.actor("cliqtomedia/historical-fx-rates-converter").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 = {
    "pairs": [
        "EUR/USD",
        "EUR/GBP",
    ],
    "lastDays": 7,
    "provider": "ECB",
    "missingDatePolicy": "none",
}

# Run the Actor and wait for it to finish
run = client.actor("cliqtomedia/historical-fx-rates-converter").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 '{
  "pairs": [
    "EUR/USD",
    "EUR/GBP"
  ],
  "lastDays": 7,
  "provider": "ECB",
  "missingDatePolicy": "none"
}' |
apify call cliqtomedia/historical-fx-rates-converter --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cliqtomedia/historical-fx-rates-converter"
        }
    }
}
```

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/ePoYBPNbimkK5WRVQ/builds/WG5d5hdSF6CoVsnh6/openapi.json
