# Exchange Rates Scraper — ECB & National Bank of Kazakhstan FX (`yadroo/fx-rates`) Actor

Fiat exchange rates for AI agents: ECB reference rates for 30 currencies plus official National Bank of Kazakhstan rates for KZT, RUB, UZS, KGS, AED and 40+ more. Latest, historical date, daily time series since 1999, conversion, 1d/7d/30d/YTD changes, wide/long output. No API key.

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

## Pricing

from $0.14 / 1,000 result items

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

Clean fiat exchange rates for AI agents, finance bots and back-office automations. Two official sources in one Actor: **European Central Bank** reference rates (30 currencies, daily since 1999, via Frankfurter) and the **National Bank of Kazakhstan** official rates (KZT plus RUB, UZS, KGS, AED, SAR, GEL and 40+ more as KZT crosses). Latest rates, one historical date, daily time series, conversion of an amount and 1d/7d/30d/YTD changes — with warnings instead of silent gaps when a currency is not covered. No API key, no proxy, no browser.

### Use cases

- **Accounting & invoicing** — official ECB / NBK rate for a given invoice date (`mode: date`) for KZ, EU and US bookkeeping.
- **FX monitoring & alerts** — schedule `latest` with `includeChanges` and alert when USD/KZT or EUR/USD moves more than X % in a day.
- **Pricing engines** — daily refresh of the full rate table (`format: wide`) for multi-currency price lists.
- **Analytics / backtests** — daily history of any ECB pair since 1999 (one pair is ~7,000 rows: set `maxItems` above that) or NBK pair for up to a year per run.
- **Treasury reporting** — convert cash balances (`mode: convert`, `amount`) at the official rate of a month-end date.
- **Agent tools** — one MCP/API call that answers "what is 2 500 USD in KZT today and how did the rate move this month?".

### Input

| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
| `mode` | string | `latest` | `latest`, `date`, `history`, `convert`, `currencies` |
| `base` | string | `USD` | ISO 4217 code, upper or lower case; must be quoted by the chosen source (see Reference). Ignored when `pairs` is set |
| `symbols` | string\[] | `[]` = all of the source | ISO codes; unsupported ones are reported, not dropped |
| `pairs` | string\[] | — | `USD/KZT`, `EUR/USD`, `USDKZT`… several bases in one run; overrides `base`/`symbols` |
| `source` | string | `auto` | `auto` (ECB, NBK for what ECB lacks), `ecb`, `nbk` |
| `date` | string | latest | `YYYY-MM-DD` (a real calendar date), modes `date`/`convert`. ECB returns the last business day ≤ date |
| `startDate` | string | `endDate` − 30 d | `YYYY-MM-DD`, mode `history` |
| `endDate` | string | today | `YYYY-MM-DD`, mode `history`. NBK history: the last 366 days of the range per run, up to tomorrow's rate |
| `amount` | string | `"1"` | Amount of `base` to convert (mode `convert`): `"2500"`, `"2 500.75"`, `"1,000,000"`; a single decimal comma (`"2,5"`) means 2.5. Anything else stops the run before any request |
| `includeChanges` | boolean | `false` | Adds `change1dPct`, `change7dPct`, `change30dPct`, `changeYtdPct` (latest/date/convert, format `long`) |
| `includeInverse` | boolean | `false` | Adds `inverseRate` = 1 / rate (format `long`) |
| `format` | string | `long` | `long` (row per date × currency) or `wide` (row per date, currencies as columns; rates only) |
| `fields` | string\[] | all columns | Keep only these columns, in this order; `id` is always kept. Columns: see Output. Case, spaces, `_` and `-` do not matter; a name that is not a column of the run is skipped and named in `SUMMARY.warnings`; if none is a column, the run stops before any request with the list of columns |
| `decimals` | integer | raw | 0–10, rounds `rate`/`inverseRate` |
| `maxItems` | integer | `5000` | 1–100 000 dataset rows (format `wide`: one per date). The cut falls on a whole date, oldest dates first — see Limits |
| `strict` | boolean | `false` | Fail the run before any request if any requested currency is unsupported |

`mode`, `source` and `format` are fixed lists (values above): Apify refuses any other value before the run starts, with the list of allowed values. Currency codes and pairs are free text: upper or lower case, and pairs as `USD/KZT`, `USD-KZT` or `USDKZT`.

### Reference

#### Sources

| `source` | Publisher | Currencies | Frequency | History | Notes |
|---|---|---|---|---|---|
| `ecb` | European Central Bank euro reference rates, served by [Frankfurter](https://frankfurter.dev) | 30 | business days, ~16:00 CET | since 1999-01-04 | cross rates via EUR; no KZT/RUB |
| `nbk` | [National Bank of Kazakhstan](https://nationalbank.kz) official rates | 48 vs KZT (+KZT) | daily, published the day before | 1990s onwards | non-KZT pairs are KZT cross rates; one request per day of history |

#### ECB set (30)

AUD, BRL, CAD, CHF, CNY, CZK, DKK, EUR, GBP, HKD, HUF, IDR, ILS, INR, ISK, JPY, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, SEK, SGD, THB, TRY, USD, ZAR

#### NBK set (48 + KZT)

AED, AMD, AUD, AZN, BRL, BYN, CAD, CHF, CNY, CZK, DKK, EGP, EUR, GBP, GEL, HKD, HUF, IDR, ILS, INR, IRR, JPY, KGS, KRW, KWD, MDL, MNT, MXN, MYR, NOK, OMR, PKR, PLN, QAR, RON, RUB, SAR, SEK, SGD, THB, TJS, TRY, UAH, USD, UZS, VND, XDR, ZAR — and KZT as base or quote.

Run `{"mode":"currencies"}` to get this dictionary as a dataset (code, name, `ecb`, `nbk`).

#### Routing in `source: auto`

| Pair | Source used |
|---|---|
| both codes in ECB set (EUR/USD, USD/JPY, GBP/CHF…) | ECB |
| any side is KZT or an NBK-only code (USD/KZT, KZT/RUB, EUR/UZS, USD/AED…) | NBK |
| code in neither set | warning `Not available…`, listed in `SUMMARY.warnings`; `strict: true` fails the run |

### Examples

**Daily FX table for a price list (all ECB currencies, one row per day)**

```json
{ "base": "EUR", "mode": "history", "startDate": "2026-08-01", "endDate": "2026-08-31", "format": "wide" }
```

**USD/KZT alert feed with momentum**

```json
{ "pairs": ["USD/KZT", "EUR/KZT", "RUB/KZT"], "includeChanges": true, "includeInverse": true, "decimals": 4 }
```

**Official rate for an invoice dated 20 March 2020 (EU and KZ books)**

```json
{ "mode": "date", "date": "2020-03-20", "pairs": ["EUR/USD", "USD/KZT"] }
```

**Convert a treasury balance**

```json
{ "mode": "convert", "base": "USD", "symbols": ["KZT", "EUR", "GBP"], "amount": "250000" }
```

**Tenge history for a backtest (NBK, one request per day)**

```json
{ "source": "nbk", "base": "KZT", "symbols": ["USD", "EUR", "RUB", "CNY"], "mode": "history", "startDate": "2026-01-01", "endDate": "2026-06-30" }
```

### Output

One item per date × base × currency (`format: long`):

```json
{
  "id": "NBK:2026-09-13:USD:KZT",
  "date": "2026-09-13",
  "base": "USD",
  "currency": "KZT",
  "currencyName": "Kazakhstani Tenge",
  "rate": 450.91,
  "inverseRate": 0.002218,
  "change1dPct": 0, "change7dPct": -1.24, "change30dPct": -3.05, "changeYtdPct": -10.28,
  "source": "NBK",
  "sourceUrl": "https://nationalbank.kz/rss/get_rates.cfm?fdate=13.09.2026",
  "fetchedAt": "2026-09-12T23:42:49.083Z"
}
```

| Field | Type | Description |
|---|---|---|
| `id` | string | `<source>:<date>:<base>:<currency>` — stable across runs |
| `date` | string | Rate date (ECB: business day the rate was fixed; NBK: official date) |
| `base`, `currency` | string | 1 `base` = `rate` × `currency` |
| `currencyName` | string | English name |
| `rate` | number | Raw source precision unless `decimals` set |
| `inverseRate` | number | Only with `includeInverse` |
| `amount`, `converted` | number | Only in mode `convert` |
| `change1dPct` … `changeYtdPct` | number | Only with `includeChanges`; % vs 1/7/30 days earlier and vs last rate of previous year |
| `source` | string | `ECB` or `NBK` |
| `sourceUrl` | string | Exact request URL |
| `fetchedAt` | string | ISO timestamp of the run |

`format: wide` emits `{ id, date, base, source, sourceUrl, fetchedAt, EUR: 0.86, GBP: 0.74, … }` — one row per source × date × base with the rates only (`inverseRate`, `amount`/`converted` and the change columns exist in format `long`). Mode `currencies` emits `{ id, code, name, ecb, nbk, sources, fetchedAt }`.

With `fields`, a row keeps `id` plus the listed columns in your order, e.g. `"fields": ["date", "rate"]` → `{ "id": …, "date": …, "rate": … }`.

The key-value store record `SUMMARY` holds `items` (rows saved), `warnings[]` (unsupported codes, skipped `fields`, switches that do not apply), `unsupported[]`, `maxItemsReached` (where the maxItems cut fell), `notRead[]` (work a spending-limit or timeout stop left unread), `failures[]` (a source request that failed while the others were saved), `status` and the run counts (`delivered`, `stoppedBy`). Every mode, `currencies` included, ends with a status message that sums this up.

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~fx-rates/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'content-type: application/json' -d '{"pairs":["USD/KZT","EUR/USD"],"includeChanges":true}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/fx-rates').call({ base: 'EUR', mode: 'history', startDate: '2026-01-01', format: 'wide' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/fx-rates").call(run_input={"mode": "convert", "base": "USD", "symbols": ["KZT"], "amount": "2500"})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

MCP: add `https://mcp.apify.com` to your agent and call the tool `yadroo/fx-rates` with the same JSON input.

### Pricing

Pay per event: **$0.001 per run start + $0.0002 per dataset row**. Typical runs: latest rates for 5 currencies ≈ $0.002; a month of history for 30 currencies (~650 rows) ≈ $0.13; a year of EUR history in wide format (~255 rows, one per date) ≈ $0.05.

Store discounts: Bronze −10 %, Silver −20 %, Gold and above −30 % on the row price; the start event is the same on every plan; platform usage is included. Warnings, unsupported codes and failed requests are never rows, so they cost nothing.

Your **Maximum cost per run** is respected: the run stops reading at it and ends with "Stopped at your spending limit: N rows delivered" (what was left unread is named in the status and `SUMMARY.notRead`).

### Limits & FAQ

- **Freshness** — ECB rates are fixed once per business day around 16:00 CET (no weekends/TARGET holidays; `latest` returns the last fixing). NBK publishes the official rate for the next day in the afternoon (Astana time), every calendar day.
- **Why is my currency missing?** — It is not in the chosen source. The run logs `Not available from …` and stores the list in `SUMMARY.warnings`; set `source: auto`, or `strict: true` to fail instead.
- **Are NBK cross rates official?** — USD/KZT, EUR/KZT etc. are the official NBK quotes. Pairs like EUR/USD from NBK are computed as `KZT-per-EUR / KZT-per-USD` and are tagged `source: NBK`; prefer ECB for non-KZT pairs (auto does).
- **maxItems** — counts dataset rows, what you are billed for (format `wide`: one row per date). History comes oldest date first, and the cut falls on a whole date, so the last date is never delivered with only some of its currencies; when even the first date (or a single `latest` snapshot) is larger than `maxItems`, its first `maxItems` rates are kept. Nothing is read ahead of the cut: an NBK history stops requesting days once `maxItems` rows are saved. The status and `SUMMARY.maxItemsReached` say where the cut fell.
- **Rate limits** — ECB history is one request per run; NBK history is one request per day with a 120 ms pause and a 366-day cap (a full year takes about 3 minutes). Up to 3 attempts with back-off on 429/5xx/timeouts. Rows are saved as they arrive (per source request, every ~50 rows of an NBK history).
- **Run timeout** — a run near its timeout stops starting new requests, saves what it has read and ends SUCCEEDED with "Stopped before the run timeout: N rows saved".
- **Errors** — invalid or impossible dates (`2026-02-30`), a range that ends before it starts, an amount that is not a number, an unsupported base, no valid pair at all, `strict` with an unsupported code, or `fields` naming no column stop the run before any request with an explanation; nothing is charged beyond the start event. A malformed pair next to valid ones is skipped with a warning. When one source request fails and others work, their rows are saved and the failure is named in the status and `SUMMARY.failures`; the run fails only when nothing could be fetched.
- **Roadmap** — more national-bank sources (CBR, NBU, CBU) on request; percent-change thresholds as a filter.

Made by **Yadroo** — see also [us-treasury-yields](https://apify.com/yadroo/us-treasury-yields), [world-bank-indicators](https://apify.com/yadroo/world-bank-indicators), [sec-company-financials](https://apify.com/yadroo/sec-company-financials), [sec-edgar-filings](https://apify.com/yadroo/sec-edgar-filings), [coingecko-markets](https://apify.com/yadroo/coingecko-markets).

# Actor input Schema

## `mode` (type: `string`):

What to fetch. `latest`: newest rate per currency (ECB = last business day, NBK = today's official rate). `date`: rates as of `date` (ECB returns the last business day on or before it). `history`: one row per day per currency between `startDate` and `endDate`, oldest date first. `convert`: like latest/date but adds `amount` and `converted`. `currencies`: dictionary of codes with which source quotes them — no rates. Only these five values are accepted: Apify refuses any other value before the run starts.

## `base` (type: `string`):

3-letter ISO 4217 code the rates are quoted against (1 base = `rate` units of `currency`); upper or lower case. Default USD. ECB set: USD, EUR, GBP, JPY, CNY, CHF and 24 more; NBK set adds KZT, RUB, UZS, KGS, AED, SAR, TRY… Ignored when `pairs` is set.

## `symbols` (type: `array`):

ISO codes to quote. Empty = every currency of the chosen source (30 for ECB, 48 for NBK). Codes not covered by the source are reported in a warning (and in the `SUMMARY` key-value record) instead of being silently dropped; with `source: auto` codes missing from the ECB set (e.g. KZT, RUB, UZS) are served by the National Bank of Kazakhstan.

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

Explicit pairs like `USD/KZT`, `EUR/USD`, `KZT/RUB` (also `USDKZT`, `USD-KZT`). Lets one run mix several bases. When set, `base` and `symbols` are ignored.

## `source` (type: `string`):

`ecb`: ECB euro reference rates (30 currencies, business days, ~16:00 CET, history since 1999). `nbk`: official daily rates of the National Bank of Kazakhstan (48 currencies against KZT; other pairs are computed as KZT cross rates). `auto` picks ECB whenever both sides are in the ECB set and NBK otherwise, and tags every row with `source`.

## `date` (type: `string`):

YYYY-MM-DD (a real calendar date). Used by modes `date` and `convert` (and `latest`); default = latest available. ECB data starts 1999-01-04, NBK history goes back to the 1990s.

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

YYYY-MM-DD (a real calendar date), first day of the time series. Default: 30 days before `endDate`.

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

YYYY-MM-DD, last day of the time series. Default: today. ECB history is one request for any range; NBK history costs one request per calendar day, is capped at the last 366 days of the range per run and ends at tomorrow's NBK rate at the latest.

## `amount` (type: `string`):

Amount of `base` to convert in mode `convert` (default 1). Adds `amount` and `converted` to each row (format long). A positive number written as a string: "2500", "2 500.75" or "1,000,000" (commas in groups of three are thousands separators; a single decimal comma like "2,5" means 2.5). Anything else stops the run before any request, with nothing charged beyond the start.

## `includeChanges` (type: `boolean`):

Modes latest/date/convert, format long: add `change1dPct`, `change7dPct`, `change30dPct`, `changeYtdPct` computed from the rates 1, 7 and 30 days earlier and from the last rate of the previous year (4 extra requests per source; a change that cannot be computed is null). Useful for alerts ("notify me when USD/KZT moves >1% in a day"). Not used by mode history or format wide (a warning says so).

## `includeInverse` (type: `boolean`):

Format long: add `inverseRate` (= 1 / rate), i.e. how many `base` units one `currency` unit buys.

## `format` (type: `string`):

`long` is the default and works for every mode. `wide` pivots rates into one row per date/base/source with a column per currency code — handy for history exports to spreadsheets. Wide rows carry rates only: `inverseRate`, `amount`/`converted` and the change columns exist in long format only. In wide format each dataset row (= one date) is one billed item and counts once against maxItems.

## `fields` (type: `array`):

Keep only these columns, in this order (`id` is always kept and comes first unless you list it elsewhere). Empty = every column. Columns: format long `date, base, currency, currencyName, rate, inverseRate, amount, converted, change1dPct, change7dPct, change30dPct, changeYtdPct, source, sourceUrl, fetchedAt` (the optional ones need their switch); format wide `date, base, source, sourceUrl, fetchedAt` + currency codes; mode currencies `code, name, ecb, nbk, sources, fetchedAt`. Case, spaces, '\_' and '-' do not matter. A name that is not a column of this run is skipped and named in SUMMARY.warnings with the reason (or a "did you mean"); when none of the names is a column, the run stops before any request with the list of columns.

## `decimals` (type: `integer`):

Optional rounding of `rate` (and `inverseRate`). Default: raw source precision.

## `maxItems` (type: `integer`):

Cap on dataset rows — what you are billed for: rate rows in format long, one row per date in format wide, codes in mode currencies. The cut falls on a whole date: a history never ends with a date that has only some of its currencies (unless the first date alone is larger than maxItems, then it keeps its first maxItems rates). History rows come oldest date first, so the cut keeps the earliest dates. The status and SUMMARY.maxItemsReached say where the cut fell. A year of ECB history for all 30 currencies is ~7,400 long rows or ~255 wide rows; ECB history since 1999 for one pair is ~7,000 rows.

## `strict` (type: `boolean`):

When true the run fails if any requested currency is not quoted by the chosen source. Default: log a warning, list them in the `SUMMARY` record and return the rest.

## Actor input object example

```json
{
  "mode": "latest",
  "base": "USD",
  "symbols": [
    "EUR",
    "GBP",
    "JPY",
    "CNY",
    "CHF"
  ],
  "source": "auto",
  "amount": "1",
  "includeChanges": false,
  "includeInverse": false,
  "format": "long",
  "maxItems": 5000,
  "strict": false
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

# 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 = {
    "base": "USD",
    "symbols": [
        "EUR",
        "GBP",
        "JPY",
        "CNY",
        "CHF"
    ],
    "amount": "1"
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/fx-rates").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 = {
    "base": "USD",
    "symbols": [
        "EUR",
        "GBP",
        "JPY",
        "CNY",
        "CHF",
    ],
    "amount": "1",
}

# Run the Actor and wait for it to finish
run = client.actor("yadroo/fx-rates").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 '{
  "base": "USD",
  "symbols": [
    "EUR",
    "GBP",
    "JPY",
    "CNY",
    "CHF"
  ],
  "amount": "1"
}' |
apify call yadroo/fx-rates --silent --output-dataset

```

## MCP server setup

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

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/4L4B8SJTLEzHWuUyf/builds/Mn18wTayFYNbVpGFT/openapi.json
