# NBP & ECB Exchange Rates - Historical Currency Converter (`danielmichalek/official-fx-rates`) Actor

Get NBP and ECB reference exchange rates, look up historical dates and convert currency amounts. Each result includes the rate date for traceable reporting. Uses public central-bank APIs, not scraped prices. Not a live trading feed or accounting advice.

- **URL**: https://apify.com/danielmichalek/official-fx-rates.md
- **Developed by:** [Daniel Michałek](https://apify.com/danielmichalek) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 amount converteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Official FX Rates (NBP + ECB)

Get official exchange rates straight from the public APIs of **Narodowy Bank Polski (NBP)** and
the **European Central Bank (ECB)**, and convert amounts with the correct rate date - including
weekend/holiday look-back and the Polish tax rule "NBP mid rate from the last business day
before the transaction" (D-1).

Typical uses: invoice and expense conversion for Polish bookkeeping (VAT, CIT/PIT), EUR
reference rates for contracts and reports, historical rate series for analysis.

Source: [European Central Bank, ECB Data Portal API](https://data-api.ecb.europa.eu/service/data/EXR/).
**ECB source data is available free of charge.** Actor charges pay for the normalization,
conversion and delivery service, not for access to ECB data. See the
[ECB copyright and reuse notice](https://www.ecb.europa.eu/services/disclaimer/html/index.en.html).
Converted amounts and cross rates are calculated by this Actor; published source observations
are unchanged. Every ECB dataset row and run summary includes this disclosure in `sourceNotice`.
NBP values come from the [official NBP Web API](https://api.nbp.pl/).

### What it does

1. Validates your input and plans the minimum number of API calls (NBP ranges are split into
   93-day chunks, the NBP limit; ECB into 366-day chunks).
2. Calls only these official endpoints:
   - NBP: `https://api.nbp.pl/api/exchangerates/...` (JSON)
   - ECB: `https://data-api.ecb.europa.eu/service/data/EXR/...` (SDMX CSV)
3. Writes one dataset item per published rate (rate list) and one per requested conversion.

Rate semantics of every rate item: `1 baseCurrency = rate quoteCurrency`.

- NBP (tables A/B, mid rates): base = foreign currency, quote = PLN.
- ECB (daily euro reference rates): base = EUR, quote = foreign currency.
- Conversions between two non-base currencies are computed as cross rates via PLN (NBP) or
  EUR (ECB), using rates of the same publication date.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `NBP` or `ECB` | `NBP` | Which central bank to use. |
| `nbpTable` | `A`, `B` or `C` | `A` | NBP only. A = major currencies (daily), B = other currencies (weekly), C = bid/ask (rate list only, not for conversions). |
| `currencies` | array of ISO codes | - | Currencies to list rates for. The base currency (PLN / EUR) is ignored. |
| `startDate` | `YYYY-MM-DD` | - | Start of the rate list. Required if `currencies` is set. |
| `endDate` | `YYYY-MM-DD` | `startDate` | End of the rate list (inclusive, max 3660 days). |
| `conversions` | array | - | `{ "amount": 100, "from": "USD", "to": "PLN", "date": "2024-01-02", "id": "optional" }`, max 10000 per run. |
| `rateDateMode` | `exact`, `onOrBefore`, `before` | `onOrBefore` | `onOrBefore`: rate of that day or the last one published before it. `before`: last rate strictly before the date (Polish D-1 tax rule). `exact`: only a rate published on that day. |
| `maxLookbackDays` | integer 0-31 | `10` | How far back a conversion may look for the last published rate. |
| `decimals` | integer 0-8 | `2` | Rounding of converted amounts (half away from zero). |

You need at least `currencies` + `startDate`, or `conversions` (or both).

Example - invoice conversion with the Polish D-1 rule:

```json
{
  "source": "NBP",
  "rateDateMode": "before",
  "conversions": [
    { "id": "FV/2024/001", "amount": 1200, "from": "EUR", "to": "PLN", "date": "2024-01-03" },
    { "id": "FV/2024/002", "amount": 99.99, "from": "USD", "to": "EUR", "date": "2024-01-08" }
  ]
}
```

Example - ECB reference rates for January 2024:

```json
{
  "source": "ECB",
  "currencies": ["USD", "PLN", "CHF"],
  "startDate": "2024-01-01",
  "endDate": "2024-01-31"
}
```

### Output

Numbers in the examples below are illustrative. Rate item:

```json
{
  "recordType": "rate",
  "source": "NBP",
  "date": "2024-01-02",
  "currency": "EUR",
  "baseCurrency": "EUR",
  "quoteCurrency": "PLN",
  "rate": 4.3434,
  "table": "A",
  "tableNo": "001/A/NBP/2024"
}
```

NBP table C items carry `bid` and `ask` instead of `rate` (`rate` is `null`).

Conversion item:

```json
{
  "recordType": "conversion",
  "id": "FV/2024/001",
  "source": "NBP",
  "amount": 1200,
  "from": "EUR",
  "to": "PLN",
  "requestedDate": "2024-01-03",
  "rateDateMode": "before",
  "status": "ok",
  "rateDate": "2024-01-02",
  "rate": 4.3434,
  "converted": 5212.08,
  "path": "direct"
}
```

`path` is `direct`, `cross via PLN` / `cross via EUR`, or `identity` (same currency). A conversion
that cannot be served has `status: "error"` and `error`:

- `RATE_NOT_FOUND` - no rate was published within the look-back window (or not on the exact day).
- `UPSTREAM_INCOMPLETE` - an API request covering that date failed; the Actor refuses to guess
  a rate from partial data.

A run summary (source, planned requests, upstream errors, counts, charged events,
`stoppedByChargeLimit`) is stored in the default key-value store under `OUTPUT`. If the API fails
and nothing billable could be produced, the run is marked as failed.

### Pricing (pay per event)

| Event | Charged when |
| --- | --- |
| `rate-record` | once per rate item written to the dataset |
| `amount-converted` | once per conversion with `status: "ok"` |

Conversions with `status: "error"` are returned but never charged. Invalid input fails the run
before any API call or charge. The price per event is shown on the Actor's **Pricing** tab in
Apify Store. If your maximum charge per run is reached, the Actor stops after the last paid item
and sets `stoppedByChargeLimit: true` in `OUTPUT`.
The existing stop policy is conservative: reaching the limit for one event type stops the
whole run, even if a cheaper event type could still fit within the remaining budget.

The Actor checks capacity before each charge and publishes a paid item only after the SDK
confirms `chargedCount: 1`. A zero budget or declined charge publishes no paid item. In a
run without pay-per-event pricing, output is still available and `chargedEvents` remains zero.
This Actor expects the two named events above; automatic dataset-item pricing is rejected
because it would also charge conversion errors.

Billing and dataset writes are separate operations, with no shared transaction. `Actor.charge`
does not expose a persistent caller idempotency key for replay after a process restart; the
underlying client generates a key for retries within a single API call. A failed write can
leave a confirmed charge without an output item; a lost
charge acknowledgement can leave an unknown billing result. A durable `FX-RUN-STATE` record
therefore blocks automatic replay after interruption and reports `RECONCILIATION_REQUIRED`.
Inspect the saved phase and confirmed counters against run billing and dataset records before
recovery; do not delete the guard or start with fresh storage as a retry. Completed runs with
the same input return the saved summary without charging or publishing again. This is not an
exactly-once guarantee and does not support concurrent processes sharing the same storage.

### Limitations

- **No scraping.** Data comes only from the official public NBP Web API and ECB Data Portal API
  listed above; no web pages are parsed and no third-party rate providers are used.
- Rates are the official published values. NBP mid rates start on 2002-01-02, ECB reference
  rates on 1999-01-04; earlier dates are rejected.
- NBP table B is published weekly; use a longer `maxLookbackDays` if needed. Currencies that are
  not in the selected table simply return no data.
- ECB reference rates are published around 16:00 CET on TARGET business days; NBP table A on
  Polish business days. Same-day requests made before publication fall back according to
  `rateDateMode`.
- Cross rates are derived arithmetically from two official rates of the same day; they are not
  themselves official quotes.
- Results depend on the availability of the upstream APIs. Transient errors (HTTP 429/5xx,
  network errors) are retried up to 3 times with back-off.
- This is data tooling, not tax or legal advice.

### Local recovery candidate

This copy preserves the existing Actor identity and ownership. Source corrections have not
been deployed to the live Actor. The local candidate package is version `0.1.1`, targeting
Node.js 22 or newer. Output links expose the dataset and `OUTPUT` summary; the dataset view
includes the ECB disclosure. `Dockerfile` and `.dockerignore` were newly authored on
2026-10-21 because the remote Dockerfile could not be read; they are not recovered originals.
The local Node 22 container build and an offline startup smoke test passed with the
preserved proxy-CA Dockerfile; see `evidence/docker-validation.json`. Production packaging
uses ordinary TLS-verified `npm ci` and prints runtime file hashes during the build.
The native import package and expected hashes are under `evidence/release-import-0.1.1/`.
Packaging was prepared without repeating those tests; hosted build, billing and deployment
remain separate checks.
Recovery provenance remains in the parent `RECOVERY_MANIFEST.json`; combined validation
evidence is in `evidence/fx-validation.json`.

# Actor input Schema

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

NBP: mid rates in PLN per unit of foreign currency (tables A/B), or bid/ask (table C). ECB: euro foreign exchange reference rates (foreign currency per 1 EUR).

## `nbpTable` (type: `string`):

Only for source NBP. A = mid rates of major currencies (daily), B = mid rates of other currencies (weekly), C = bid/ask rates (rate list only, not usable for conversions).

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

ISO 4217 codes whose rates should be listed for the date range below. The source's own base currency (PLN for NBP, EUR for ECB) is ignored. Leave empty if you only need conversions.

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

First day of the rate list (YYYY-MM-DD). Required when currencies are given.

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

Last day of the rate list (YYYY-MM-DD, inclusive). Defaults to the start date. Maximum range: 3660 days.

## `conversions` (type: `array`):

Objects { "amount": 100, "from": "USD", "to": "PLN", "date": "YYYY-MM-DD", "id": "optional" }. Cross rates are computed via the source's base currency. At most 10000 conversions per run.

## `rateDateMode` (type: `string`):

onOrBefore: rate of the given date or the last published before it (weekends/holidays). before: last rate strictly before the date (Polish tax rule: NBP rate from the last business day preceding the transaction). exact: only a rate published on that very date.

## `maxLookbackDays` (type: `integer`):

How many calendar days back a conversion may search for the last published rate.

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

Converted amounts are rounded half away from zero to this many decimal places.

## Actor input object example

```json
{
  "source": "NBP",
  "nbpTable": "A",
  "currencies": [
    "EUR",
    "USD"
  ],
  "startDate": "2024-01-02",
  "endDate": "2024-01-31",
  "conversions": [
    {
      "id": "inv-1",
      "amount": 100,
      "from": "USD",
      "to": "PLN",
      "date": "2024-01-02"
    },
    {
      "id": "inv-2",
      "amount": 250.5,
      "from": "EUR",
      "to": "PLN",
      "date": "2024-01-06"
    }
  ],
  "rateDateMode": "onOrBefore",
  "maxLookbackDays": 10,
  "decimals": 2
}
```

# 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 = {
    "currencies": [
        "EUR",
        "USD"
    ],
    "startDate": "2024-01-02",
    "endDate": "2024-01-31",
    "conversions": [
        {
            "id": "inv-1",
            "amount": 100,
            "from": "USD",
            "to": "PLN",
            "date": "2024-01-02"
        },
        {
            "id": "inv-2",
            "amount": 250.5,
            "from": "EUR",
            "to": "PLN",
            "date": "2024-01-06"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("danielmichalek/official-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 = {
    "currencies": [
        "EUR",
        "USD",
    ],
    "startDate": "2024-01-02",
    "endDate": "2024-01-31",
    "conversions": [
        {
            "id": "inv-1",
            "amount": 100,
            "from": "USD",
            "to": "PLN",
            "date": "2024-01-02",
        },
        {
            "id": "inv-2",
            "amount": 250.5,
            "from": "EUR",
            "to": "PLN",
            "date": "2024-01-06",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("danielmichalek/official-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 '{
  "currencies": [
    "EUR",
    "USD"
  ],
  "startDate": "2024-01-02",
  "endDate": "2024-01-31",
  "conversions": [
    {
      "id": "inv-1",
      "amount": 100,
      "from": "USD",
      "to": "PLN",
      "date": "2024-01-02"
    },
    {
      "id": "inv-2",
      "amount": 250.5,
      "from": "EUR",
      "to": "PLN",
      "date": "2024-01-06"
    }
  ]
}' |
apify call danielmichalek/official-fx-rates --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,danielmichalek/official-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/4sqJ1bPZZKeD5U180/builds/XniFzBSMGQvcxbVFp/openapi.json
