The complete REST and WebSocket contracts: authentication, limits, response fields, endpoints, and errors. If you are making your first request, the quickstart is the faster place to begin.
Introduction
exchangerate.dev is a REST API for indicative foreign exchange rates. It provides live intraday rates on trading days for all 144 currencies, with source, market_session, and freshness fields on every response. Try the free exchange rate API anonymously, or start with a free key: no credit card required.
Quickstart
Make your first data call without signup. The standalone 60-second quickstart has the request, exact response shape, and optional API-key step.
Authentication
You can evaluate data routes without a key. Pass your key in the Authorization: Bearer header for account-tier quotas and attribution. Anonymous access is limited to 10 requests/minute and 100 requests/hour per IP. Never embed an account key in public browser JavaScript; use anonymous access or a server-side proxy. Account quota resets at 00:00 UTC on the 1st.
WebSocket clients authenticate in their first message. Do not put an API key in the WebSocket URL. Anonymous streaming is unavailable.
| Name | Type | Description |
|---|---|---|
Authorization | header | Bearer <your-key>. Optional for anonymous evaluation; required for account-tier quotas. |
X-API-Key | header | Alias for Authorization: Bearer. Bearer takes precedence when both are supplied. |
X-RateLimit-Limit | response | Call limit for the current window. |
X-RateLimit-Remaining | response | Calls remaining in the current window. |
X-RateLimit-Reset | response | Unix timestamp when the window resets. |
X-RateLimit-Scope | response | On limit errors: tier-rate-minute, tier-quota-month, or anon-ip-hour. |
Rate limits
Free, keyless callers, and exchangerate.dev pages receive live rates 15 minutes late; Basic, Plus and Pro receive the same feed live. Delayed responses carry X-Price-Delay-Seconds: 900. ECB daily fixes are published once daily without delay. Free and Basic have no WebSocket; Plus and Pro include it. Without an ECB fix, daily history (ranges, dates, daily time series) shows closed days only after midnight UTC; today appears tomorrow.
Anonymous limits are per IP; account limits are per key and calendar month. On 429 the response includes Retry-After.
| Plan | Monthly calls | Rate limit | Price |
| Free | 5,000/month | 10 req/min | $0/mo |
| Basic | 100,000/month | 60 req/min | $19/mo |
| Plus | 300,000/month | 120 req/min | $39/mo |
| Pro | 1,000,000/month | 300 req/min | $89/mo |
For new accounts, stream caps are separate from REST requests per minute: Free: 0 concurrent connections, 0 pairs per connection; Basic: 0 concurrent connections, 0 pairs per connection; Plus: 1 concurrent connection, 10 pairs per connection; Pro: 5 concurrent connections, 100 pairs per connection. Existing accounts keep their earlier limits; the authenticated WebSocket frame reports effective caps. An accepted stream connection uses one monthly quota unit.
Response metadata
Keep this metadata with the rate. It is the API's freshness and derivation context.
| Field | Meaning and applicability |
|---|---|
source and sources | source is the least-fresh contributing rate; sources maps returned currencies. Enum values: live (intraday indicative), ecb_daily (ECB fix), and market_daily (stored daily market-feed close for currencies without an ECB fix, used in date history and daily time series). |
market_session | open, weekend, or interbank_closed; it describes the market state for the response. |
data_updated_at, effective_at, and timestamp | data_updated_at is the oldest contributing observation. effective_at maps each currency to its own observation time on latest and batch-convert responses. timestamp is when this response was served. Times are UTC ISO-8601 strings. |
derived_symbols, derived, and derivation_bps_max | derived_symbols lists triangulated rates in multi-currency responses. Single-pair responses use derived; derivation_bps_max can be null, including for a derived pair without a fixed ceiling, so use derived to detect triangulation. |
notice | A short reminder that rates are indicative, not for settlement. Daily-reference responses include the reference-data notice. |
Operation registry
Every public integration operation is listed once by method and OpenAPI path. See the OpenAPI 3.1 schema or ReDoc for complete schemas.
Path and query base values must match when both are sent; conflicts return 400.
{ "result": "success", "base": "USD", "source": "live", "sources": { "EUR": "live", "GBP": "live" }, "market_session": "open", "timestamp": "2026-06-15T12:00:00Z", "data_updated_at": "2026-06-15T12:00:00Z", "effective_at": { "EUR": "2026-06-15T12:00:00Z", "GBP": "2026-06-15T12:00:00Z" }, "rates": { "EUR": 0.86207, "GBP": 0.74627 }, "stale": { "EUR": false, "GBP": false }, "derived_symbols": [], "notice": "Indicative rates, not for settlement."}| Operation | Purpose | Parameters | Request | Response | Endpoint errors |
|---|---|---|---|---|---|
POST /v1/account/checkout | Open checkout or subscription management for the API key's organization. | API key auth; JSON body: plan (basic_2026/plus_2026/pro_2026), interval (monthly/yearly) | POST /v1/account/checkout | CheckoutResponse | authentication, checkout in progress, or billing unavailable |
POST /v1/account/checkout-session | Open Stripe checkout or subscription management for the signed-in account. | session auth; JSON body: plan (basic_2026/plus_2026/pro_2026), interval (monthly/yearly) | POST /v1/account/checkout-session | CheckoutResponse | authentication, checkout in progress, or billing unavailable |
GET /v1/latest | Latest rates with USD as the default base. | base, path_base, symbols (query; optional) | /v1/latest?base=USD&symbols=EUR,GBP | LatestResponse | data and limit errors |
GET /v1/latest/{path_base} | Latest rates with a path base. | path_base (path); base, symbols (query); path/query base must match | /v1/latest/USD?symbols=EUR,GBP | LatestResponse | conflicting base, data, and limit errors |
GET /v1/{date} | Historical snapshot for a date. | date (path); base, path_base, symbols (query) | /v1/2024-01-15?base=EUR&symbols=USD,GBP | HistoricalResponse | bad date or unavailable data |
GET /v1/{date}/{path_base} | Historical snapshot with a path base. | date, path_base (path); base, symbols (query); path/query base must match | /v1/2024-01-15/EUR?symbols=USD,GBP | HistoricalResponse | conflicting base, bad date, or unavailable data |
GET /v1/convert/{from_ccy}/{to_ccy} | Convert one unit or a query-supplied amount. | from_ccy, to_ccy (path); amount (query, default 1) | /v1/convert/USD/EUR?amount=100 | ConvertResponse | invalid pair, amount, or unavailable data |
GET /v1/convert/{from_ccy}/{to_ccy}/{amount} | Convert a path-supplied amount. | from_ccy, to_ccy, amount (path) | /v1/convert/USD/EUR/100 | ConvertResponse | invalid pair, amount, or unavailable data |
POST /v1/convert | Convert several amounts in one request. | JSON body: from and pairs (required) | POST /v1/convert | ConvertBatchResponse | invalid request, amount, or unavailable data |
GET /v1/range | Daily time series with JSON, CSV, or Parquet output. | symbols, start_date, end_date required by API validation; base, cursor, format optional | /v1/range?base=USD&symbols=EUR,GBP&start_date=2024-01-01&end_date=2024-06-30 | RangeResponse | missing parameter, bad date, or unavailable data |
GET /v1/timeseries | Market observations from 1m through 4h, or existing daily reference history at 1d. | symbols, interval, from, to required; base defaults to USD; cursor and format optional | /v1/timeseries?base=USD&symbols=EUR,JPY&interval=5m&from=2026-09-14T12:00:00Z&to=2026-09-14T13:00:00Z | TimeseriesResponse | unsupported interval, missing parameter, bad date, or unavailable data |
GET /v1/indices/{index_slug} | Latest complete value for the six-currency reconstruction or 13-currency USD strength index. | index_slug (path): dxy-reconstructed or usd-strength | /v1/indices/usd-strength | IndexSnapshotResponse | unknown index or incomplete/misaligned basket |
GET /v1/indices/{index_slug}/timeseries | Complete aligned index observations at a public interval. | index_slug (path); interval, from, to required; cursor optional | /v1/indices/usd-strength/timeseries?interval=1d&from=2026-01-02T00:00:00Z&to=2026-09-15T23:59:59Z | IndexTimeseriesResponse | unknown index, unsupported interval, bad date, or unavailable data |
GET /v1/currencies | List supported currencies and their display facts. | none | /v1/currencies | CurrenciesResponse | standard endpoint errors |
GET /v1/rate/{slug} | Return one flat pair quote by slug. | slug (path) | /v1/rate/eur-usd | RateResponse | invalid pair or unavailable data |
POST /v1/account/provision | Create an account organization, with optional first-key creation. | session auth; optional JSON body: defer_key, signup_attribution, marketing_consent | POST /v1/account/provision | ApiKeyCreatedResponse | authentication or account errors |
GET /v1/account | Read account, active key, and monthly usage. | session authentication | /v1/account | AccountResponse | authentication or account errors |
GET /v1/account/keys | List the signed-in account's current and retiring API keys. | session authentication | /v1/account/keys | key summaries and five-key limit | authentication or account errors |
POST /v1/account/keys | Create, rename, rotate, or revoke an API key in the signed-in account. | session auth; JSON body: action, optional id, request_id, name | POST /v1/account/keys | managed key | authentication, validation, or key-limit errors |
GET /v1/account/usage/daily | Read daily usage for up to 90 days. | session auth; days (query, optional) | /v1/account/usage/daily?days=30 | DailyUsageResponse | authentication or account errors |
DELETE /v1/account | Cancel billing and delete account data. | session authentication | DELETE /v1/account | DeleteAccountResponse | authentication, account, or billing errors |
GET /v1/account/webhooks | List the account's webhook endpoints, the plan's endpoint limit and the event types. | session authentication | /v1/account/webhooks | webhooks, limit, events | authentication or account errors |
POST /v1/account/webhooks | Add a webhook endpoint for official.published and official.corrected events. The signing secret is returned once. | session auth; JSON body: url, events, optional description (email_fallback is not offered yet and returns 400) | POST /v1/account/webhooks | endpoint with secret (201) | authentication, plan limit (Plus 5, Pro 25), or URL and event validation errors |
DELETE /v1/account/webhooks/{endpoint_id} | Remove a webhook endpoint. | session auth; endpoint_id (path) | DELETE /v1/account/webhooks/{endpoint_id} | 204 No Content | authentication or not_found |
POST /v1/account/webhooks/{endpoint_id}/rotate-secret | Replace an endpoint's signing secret. The new secret is returned once. | session auth; endpoint_id (path) | POST /v1/account/webhooks/{endpoint_id}/rotate-secret | id, secret | authentication or not_found |
POST /v1/account/webhooks/{endpoint_id}/test | Queue a test event to one endpoint. | session auth; endpoint_id (path) | POST /v1/account/webhooks/{endpoint_id}/test | queued, event_id (202) | authentication or not_found |
POST /v1/keys/rotate | Rotate the active API key with a grace period. | session authentication | POST /v1/keys/rotate | ApiKeyCreatedResponse | authentication or account errors |
DELETE /v1/keys | Revoke the active API key. | session authentication | DELETE /v1/keys | success envelope | authentication or account errors |
POST /v1/consent | Record acceptance of a policy version. | session auth; JSON body: policy_version (required) | POST /v1/consent | ConsentResponse | authentication or validation errors |
GET /v1/latest
Latest rates for all supported pairs, or a filtered subset via ?symbols=.
GET /v1/latest[/{BASE}]# Examplescurl /v1/latest/USD # base=USD (default)curl /v1/latest/EUR?symbols=USD,GBP,JPY # EUR base, 3 symbols| Name | Type | Description |
|---|---|---|
base | string | Base currency (default USD). Also accepts path: /v1/latest/EUR. |
symbols | string | Comma-separated list to filter rates (e.g. EUR,GBP). Omit for all. |
GET /v1/{YYYY-MM-DD}
Returns the reference rate as published on that date. Published coverage starts on 1999-01-04 for Plus and Pro; Free and keyless history cover the last 30 days, Basic the last year. The historical data guide covers weekends, ranges, pagination, and downloads.
curl "https://api.exchangerate.dev/v1/2024-01-15/EUR?symbols=USD,GBP" \ -H "Authorization: Bearer YOUR_API_KEY"# Daily history this old requires Plus or Pro# On a weekend or holiday: returns the most recent published fix# is_forward_filled: true will be set in the responseGET /v1/convert
Single or batch currency conversion. The POST form accepts multiple pairs in one call.
# SingleGET /v1/convert/USD/EUR/100# Batch POST (counts as one call, up to 100 pairs)POST /v1/convert{ "from": "USD", "pairs": [["EUR", 100], ["GBP", 50], ["JPY", 1000]]}GET /v1/rate/{slug}
Returns a flat, single-pair quote by slug (e.g. eur-usd). This differs from /v1/convert, which nests per-pair results under conversions[] and adds amount/converted. Slug is case-insensitive; malformed or unknown pairs return invalid_pair.
GET /v1/rate/{slug}curl https://api.exchangerate.dev/v1/rate/eur-usd \ -H "Authorization: Bearer YOUR_KEY"# Response{ "result": "success", "pair": "eur-usd", "base": "EUR", "quote": "USD", "rate": 1.0824, "source": "live", "sources": { "USD": "live" }, "stale": false, "market_session": "open", "timestamp": "2026-06-16T14:02:11Z", "data_updated_at": "2026-06-16T14:01:41Z", "derived": false, "derivation_bps_max": null, "notice": "Indicative rates, not for settlement."}GET /v1/range
Daily time-series for a date window. JSON responses are capped at 366 rows/page with keyset pagination; pass format=csv or format=parquet to download the full window as a single file.
curl "https://api.exchangerate.dev/v1/range?base=USD&symbols=EUR,GBP&start_date=2024-01-01&end_date=2024-06-30" \ -H "Authorization: Bearer YOUR_API_KEY"# This range requires Plus or Pro# Response includes next_cursor + has_more for paginationGET /v1/timeseries
Use a Pro key for the 15m example on the intraday exchange rate API page. Plus includes 1h and 4h for 30 days.
Aligned rate observations for 1m, 5m, 15m, 1h, 4h, or 1d. Plus: 1h/4h, last 30 days; Pro: 1m to 4h, full depth. The daily interval uses ECB fixes for about 30 currencies and market daily closes for the other 113 currencies.
curl "https://api.exchangerate.dev/v1/timeseries?base=EUR&symbols=JPY&interval=5m&from=2026-09-14T12:00:00Z&to=2026-09-14T13:00:00Z" \ -H "Authorization: Bearer YOUR_KEY"# Pro required for 5m. JSON uses next_cursor + has_more. CSV and Parquet export one file.| Name | Type | Description |
|---|---|---|
base | string | Three-letter base currency; defaults to USD. |
symbols | string | Required comma-separated quote currencies. |
interval | enum | Required: 1m, 5m, 15m, 1h, 4h, or 1d. |
from | timestamp | Required inclusive UTC ISO-8601 start. |
to | timestamp | Required inclusive UTC ISO-8601 end. |
cursor | timestamp | JSON keyset cursor returned by the prior page. |
format | enum | json (default), csv, or parquet. |
JSON pages contain up to 1,000 timestamps. Maximum windows are 7 days for 1m, 31 days for 5m, 93 days for 15m, 366 days for 1h, and 1,830 days for 4h or 1d. Missing market observations stay missing. For intraday derived crosses, both required legs must be in the same interval bucket and their closing observations must be no more than five seconds apart. The derived value keeps the oldest contributing observation timestamp; it is a derived rate observation, not an exact synchronized OHLC close. The row timestamp is its bucket start; effective_at maps each symbol to its actual observation time. Candle backfills have null observation times and serve direct rates only. CSV and Parquet use SYMBOL_effective_at columns. A null observation timestamp and a missing row indicate unavailable data; clients should not forward-fill them.
Market collection began on 2026-09-14. The example above uses a populated trading-hour window from that day. Retention is not the amount of history already collected: 1m rows are retained for 30 days; 5m and coarser rows have no scheduled expiry. Availability still varies by pair and window, and the current open bucket can change.
Pair and interval coverage
WebSocket covers 14 currencies: AUD, CAD, CHF, EUR, GBP, JPY, MXN, NOK, NZD, PLN, SEK, SGD, USD, ZAR. Intraday history covers 221 pairs at 1m: every USD pair plus 78 crosses. EUR/USD and USD/EUR count as one unique pair in this table; self-pairs are excluded. Latest-rate freshness on the currency table does not establish intraday or stream eligibility.
| Interval | Unique pairs | Data |
|---|---|---|
1m | 221 | Market observations |
5m | 221 | Market observations |
15m | 221 | Market observations |
1h | 221 | Market observations |
4h | 209 | Market observations |
1d | 10296 | Daily reference across 144 currencies |
| WebSocket | 91 | Live market observations |
At 4h, the 12 non-USD SGD crosses are excluded; USD/SGD and SGD/USD remain supported. HKD, THB, TRY and other currencies outside the WebSocket list have daily history and intraday USD-pair coverage. There is no 2h interval. An eligible pair may still have missing or stale observations.
An unknown interval returns HTTP 400 unsupported_interval. A known interval with an unsupported pair returns HTTP 400 unsupported_pair_interval with rejected_pairs; mixed requests fail in full. A supported request with no observations returns HTTP 200 success, data: [], has_more: false and next_cursor: null. An empty result is not a service error.
{ "result": "error", "code": "unsupported_pair_interval", "message": "requested pair is outside approved time-series coverage", "rejected_pairs": ["EUR/SGD"]}GET /v1/indices
Two versioned USD indices share the same source and alignment rules as the rate API. dxy-reconstructed applies the published six-currency basket formula to exchangerate.dev observations. It is an independent reconstruction, not the official index. usd-strength is an equal-weight geometric basket of 13 currencies with 2026-01-02 = 100.
curl "https://api.exchangerate.dev/v1/indices/usd-strength" \ -H "Authorization: Bearer YOUR_KEY"curl "https://api.exchangerate.dev/v1/indices/dxy-reconstructed/timeseries?interval=1d&from=2026-01-02T00:00:00Z&to=2026-09-15T23:59:59Z" \ -H "Authorization: Bearer YOUR_KEY"Latest responses include the value, methodology version, weights, constituent rates, effective times, and log contributions. History accepts every public interval and the same maximum windows as /v1/timeseries. Every constituent must be present. Intraday timestamps must be within 30 seconds; otherwise the point stays missing and the weights are not redistributed.
Start with the basket comparison and worked API examples; use this reference for the index response fields and limits.
WebSocket /v1/stream
Connect to wss://api.exchangerate.dev/v1/stream, authenticate, then subscribe by base currency and symbol list. The same 14 currencies above form 91 unique pairs (182 directions). Authenticate within 10 seconds of opening the connection. The service sends an initial snapshot, rate updates, heartbeat frames, and explicit stale or unavailable states. Each requested pair emits at most one update per second. While the interbank week is closed, state frames report unavailable with reason market_closed and next_open. Snapshot and update frames report market_session: weekend for the current time; stale follows the same rule as /v1/latest.
Run this example on a server with Node.js 22 or later and set EXCHANGERATE_API_KEY in its environment. Keep the key off public browser pages. Derived stream pairs require both legs to be fresh and no more than five seconds apart; otherwise the pair is unavailable or stale.
const ws = new WebSocket("wss://api.exchangerate.dev/v1/stream");ws.addEventListener("open", () => { ws.send(JSON.stringify({ action: "auth", api_key: process.env.EXCHANGERATE_API_KEY }));});ws.addEventListener("message", ({ data }) => { const message = JSON.parse(data); if (message.type === "authenticated") { ws.send(JSON.stringify({ action: "subscribe", base: "EUR", symbols: ["USD", "JPY"] })); ws.send(JSON.stringify({ action: "subscribe_index", index: "usd-strength" })); } console.log(message);});{ "type": "update", "base": "EUR", "symbol": "JPY", "rate": "180.12", "effective_at": "2026-09-14T09:00:10Z", "source": "live", "source_type": "market", "market_session": "open", "derived": true, "stale": false}Send {"action":"unsubscribe","base":"EUR","symbols":["JPY"]} to remove pairs, or {"action":"unsubscribe_index","index":"usd-strength"} to remove an index. Index subscriptions do not consume the pair allowance. Reconnect with exponential backoff after a network failure. Error codes distinguish invalid authentication, exhausted monthly quota, connection and subscription caps, malformed messages, and temporary stream unavailability.
Build a reconnecting server-side WebSocket client with the integration guide, then use this reference for every frame and error.
GET /v1/currencies
Full list of supported currencies with name, type, decimal precision, minor units, and derived flag. This endpoint takes no parameters.
GET /v1/currenciesGET /v1/account
Your org, active key, and current-month usage.
{ "result": "success", "email": "you@example.com", "org": { "id": "...", "tier": "pro_2026", "status": "active" }, "key": { "prefix": "exr_live_ab12", "plaintext": null, "created_at": "2026-06-01T00:00:00Z", "last_used_at": "2026-07-01T18:02:00Z" }, "usage": { "calls_this_month": 142820, "limit": 1000000, "resets_on": "2026-08-01" }}Errors
Every REST error uses the same envelope. A 429 response also includes a Retry-After header.
{ "result": "error", "code": "missing_parameter", "message": "required parameter missing: symbols"}Data errors: invalid_pair, unsupported_base, unsupported_pair_interval, bad_date, invalid_amount, invalid_request, missing_parameter, data_unavailable, no_data_for_date, live_unavailable, source_unavailable, and service_unavailable. Time-series validation also emits unsupported_interval.
Plan access errors return HTTP 403 with plan_gated, tier and interval. History-depth errors also include history_days and earliest_allowed. When an eligible upgrade allows the request, upgrade_plans, upgrade_offers (plan and loses), and upgrade_url are included.
{ "result": "error", "code": "plan_gated", "message": "The 15m interval is not included in your plan.", "tier": "plus_2026", "interval": "15m", "upgrade_plans": ["pro_2026"], "upgrade_offers": [{ "plan": "pro_2026", "loses": [] }], "upgrade_url": "https://exchangerate.dev/pricing?plan=pro_2026&source=api&reason=plan_gated"}WebSocket subscriptions reject an unknown currency with unknown_currency and a pair outside live coverage with unsupported_stream_pair. Mixed valid and invalid symbol lists are rejected as one request; no partial subscription is created.
Authentication and limit errors: invalid_api_key, auth_unavailable, forbidden, rate_limited, ip_rate_limited, quota_exceeded, and quota_unavailable.
Python
Use any HTTP client. This standard-library example includes a timeout and reads the same response shown above. The Python integration guide has a compact production example.
from urllib.request import Request, urlopenimport json# Identify your application explicitly when calling the API.request = Request("https://api.exchangerate.dev/v1/latest/USD?symbols=EUR,GBP", headers={"User-Agent": "exchangerate-docs/1.0"})with urlopen(request, timeout=10) as response: data = json.load(response)print(data["rates"]["EUR"])Node
Use built-in fetch or another HTTP client. This example checks the HTTP status and uses a ten-second timeout. The JavaScript conversion guide covers server-safe use.
const response = await fetch("https://api.exchangerate.dev/v1/latest/USD?symbols=EUR,GBP", { signal: AbortSignal.timeout(10_000),});if (!response.ok) throw new Error(`HTTP ${response.status}`);const data = await response.json();console.log(data.rates.EUR);Example response
Numbers and timestamps change as rates update; the response shape and freshness fields are stable.
{ "result": "success", "base": "USD", "source": "live", "sources": { "EUR": "live", "GBP": "live" }, "market_session": "open", "timestamp": "2026-06-15T12:00:00Z", "data_updated_at": "2026-06-15T12:00:00Z", "effective_at": { "EUR": "2026-06-15T12:00:00Z", "GBP": "2026-06-15T12:00:00Z" }, "rates": { "EUR": 0.86207, "GBP": 0.74627 }, "stale": { "EUR": false, "GBP": false }, "derived_symbols": [], "notice": "Indicative rates, not for settlement."}MCP server
Use exchangerate.dev as a tool in any MCP-compatible AI agent. The dedicated MCP quickstart has setup, authentication, and the tool table.