API documentation
A field guide to current rates, conversions, keys, and request limits.
Authentication
Metered endpoints require an API key in the Authorization: Bearer <key> header. Get or create a key in API keys. The key is returned only once when created; keep it private and send requests over HTTPS in production.
Authorization header
HTTPSAuthorization: Bearer YOUR_API_KEYQuotas and limits
The monthly quota applies per user across all keys, per UTC calendar month. Each plan sets its own quota and per-second limit; see Plans and limits. Accepted responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Quota and rate rejections return Retry-After.
Plans and limits
Each plan sets these limits. Compare them, with prices, on the pricing section.
- Monthly quota
- Every accepted request counts once toward the quota of your plan, across all of your keys. Rejected requests do not count. When the quota is used up, the API answers
429 API_QUOTA_EXCEEDEDwithRetry-Afterset to the seconds until the quota resets at 00:00 UTC on the 1st of the next month. You receive an email at 80% and at 100% of the quota. - Per-second limit
- Each key may make up to the number of requests per second its plan allows. Going over returns
429 API_RATE_LIMITEDwithRetry-After: 1. The limit applies to each key separately. Requests rejected this way do not count toward the monthly quota. - Update interval
- How often your plan can receive a new edition. Your plan sees a new edition at most that often. Responses include
updatedAt(when the current edition was published) andnextUpdateAt(when the next one is due). - Webhooks and stream
- Paid plans can push rates to you. Starter allows 1 webhook and 1 WebSocket connection, Pro 3 and 3, and Business 5 and 5. The free plan has neither; trying answers
403 RATES_PLAN_UPGRADE_REQUIRED(close code4403on the stream). - Historical rates
- How far back
/historicaland/timeseriesreach. The free plan reaches the last 30 days; Starter, Pro, and Business reach the full archive since 1999. An older date answers403 RATES_HISTORY_NOT_IN_PLAN.
Endpoints
Base path /api/v1; API base URL https://api.currencyexchangerate-api.com/api/v1.
Using an AI assistant? The same data is available as an MCP server.
Import the OpenAPI 3.1 description or the Postman collection to try every endpoint.
GET /latest
Returns current USD-based rates. Metered; requires a key.
| Parameter | Required | Description |
|---|---|---|
| base | No | Currency the rates are per one unit of, case-insensitive; default USD. Any currency in the latest snapshot works. |
| symbols | No | Comma-separated currency codes, at most 200, case-insensitive. Omit to return all. |
Latest rates
cURL · shellcurl --fail-with-body -sS "https://api.currencyexchangerate-api.com/api/v1/latest?symbols=EUR,GBP" \ -H "Authorization: Bearer $EXCHANGE_API_KEY"Response
JSON{ "base": "USD", "updatedAt": "2026-10-08T00:00:01.000Z", "nextUpdateAt": "2026-10-09T00:00:01.000Z", "rates": { "EUR": 0.8931, "GBP": 0.7567 }}Set base to read the rates from any currency in the snapshot. Each rate is derived from the same edition: EGP to EUR is usd[EUR] / usd[EGP], so any pair works from any base. Without symbols, the response includes the base itself at 1.
Request with base
HTTPS · GETcurl "https://api.currencyexchangerate-api.com/api/v1/latest?base=EGP&symbols=EUR,USD" \ -H "Authorization: Bearer YOUR_API_KEY"Response with base
JSON{ "base": "EGP", "updatedAt": "2026-10-08T00:00:01.000Z", "nextUpdateAt": "2026-10-09T00:00:01.000Z", "rates": { "EUR": 0.01895679012, "USD": 0.02057613169 }}GET /convert
Converts any supported pair through USD. Metered; requires a key.
| Parameter | Required | Description |
|---|---|---|
| from | Yes | Source currency code, case-insensitive. |
| to | Yes | Target currency code, case-insensitive. |
| amount | No | Greater than 0 and at most 1,000,000,000,000; defaults to 1. |
Convert
cURL · shellcurl --fail-with-body -sS "https://api.currencyexchangerate-api.com/api/v1/convert?from=EUR&to=GBP&amount=100" \ -H "Authorization: Bearer $EXCHANGE_API_KEY"Response
JSON{ "from": "EUR", "to": "GBP", "amount": 100, "rate": 0.8472735416, "result": 84.727354, "updatedAt": "2026-10-08T00:00:01.000Z"}GET /historical
Returns the closing rates of one past UTC day. On a weekend or holiday the nearest earlier day with rates is served, and date says which day it is. Metered; requires a key. Free plans reach back 30 days; paid plans reach the full archive since 1999.
| Parameter | Required | Description |
|---|---|---|
| date | Yes | YYYY-MM-DD (UTC), no later than today and within your plan’s reach. |
| base | No | Currency the rates are per one unit of, case-insensitive; default USD. |
| symbols | No | Comma-separated currency codes, at most 200, case-insensitive. Omit to return all. |
Request
HTTPScurl "https://api.currencyexchangerate-api.com/api/v1/historical?date=2026-10-10&base=EUR&symbols=USD,GBP" \ -H "Authorization: Bearer YOUR_API_KEY"JavaScript fetch
JavaScriptconst response = await fetch('https://api.currencyexchangerate-api.com/api/v1/historical?date=2026-10-10&base=EUR&symbols=USD,GBP', { headers: { Authorization: 'Bearer YOUR_API_KEY' },});const data = await response.json();Response
JSON{ "requestedDate": "2026-10-10", "date": "2026-10-09", "base": "EUR", "rates": { "GBP": 0.8524910453, "USD": 1.085422772 }}GET /timeseries
Returns closing rates for every day with rates from start to end inclusive, at most 366 days, keyed by date. Days without rates, such as weekends in the ECB archive, are left out. One request counts once, however long the range. Metered; requires a key.
| Parameter | Required | Description |
|---|---|---|
| start | Yes | First day, YYYY-MM-DD (UTC), within your plan’s reach. |
| end | Yes | Last day, YYYY-MM-DD (UTC), no later than today and at most 365 days after start. |
| base | No | Currency the rates are per one unit of; default USD. Days without the base are left out. |
| symbols | No | Comma-separated currency codes, at most 200. Codes a day lacks are left out of that day. |
Request
HTTPScurl "https://api.currencyexchangerate-api.com/api/v1/timeseries?start=2026-10-05&end=2026-10-07&symbols=EUR" \ -H "Authorization: Bearer YOUR_API_KEY"JavaScript fetch
JavaScriptconst response = await fetch('https://api.currencyexchangerate-api.com/api/v1/timeseries?start=2026-10-05&end=2026-10-07&symbols=EUR', { headers: { Authorization: 'Bearer YOUR_API_KEY' },});const data = await response.json();Response
JSON{ "base": "USD", "start": "2026-10-05", "end": "2026-10-07", "rates": { "2026-10-05": { "EUR": 0.9205 }, "2026-10-06": { "EUR": 0.9198 }, "2026-10-07": { "EUR": 0.9213 } }}GET /currencies
Returns every code in the newest snapshot, sorted by code, with an English display name. Public and not metered. Cache-Control: public, max-age=3600.
Request
HTTPScurl https://api.currencyexchangerate-api.com/api/v1/currenciesJavaScript fetch
JavaScriptconst response = await fetch('https://api.currencyexchangerate-api.com/api/v1/currencies');const data = await response.json();Response
JSON[ { "code": "AED", "name": "United Arab Emirates Dirham" }]GET /preview
Returns a fixed selection of current rates. Public and not metered; no key required. Missing currencies are skipped. Cache-Control: public, max-age=300.
Request
HTTPScurl https://api.currencyexchangerate-api.com/api/v1/previewJavaScript fetch
JavaScriptconst response = await fetch('https://api.currencyexchangerate-api.com/api/v1/preview');const data = await response.json();Response
JSON{ "base": "USD", "updatedAt": "2026-10-08T00:00:01.000Z", "nextUpdateAt": "2026-10-09T00:00:01.000Z", "rates": { "EUR": 0.8931, "GBP": 0.7567 }}Errors
Errors use application/problem+json and include a machine-readable code.
| Code | Status | Meaning |
|---|---|---|
| RATES_INVALID_INPUT | 422 | A query parameter is missing or invalid. |
| RATES_UNSUPPORTED_CURRENCY | 422 | Code is validly formed but absent from the snapshot. |
| RATES_UNAVAILABLE | 503 | No snapshot is available. |
| API_KEY_INVALID | 401 | Missing or invalid API key. |
| API_QUOTA_EXCEEDED | 429 | Monthly quota is used. |
| API_RATE_LIMITED | 429 | Request limit exceeded. |
| RATES_HISTORY_NOT_IN_PLAN | 403 | The date is older than your plan’s historical reach. |
| RATES_HISTORY_UNAVAILABLE | 404 | No rates are stored for the date or the 7 days before it. |
Data
Rates are USD-based and cover about 160 currencies. Responses include updatedAt and nextUpdateAt. Current endpoints serve the newest edition for your plan.
Rates are mid-market reference rates compiled from multiple central-bank and commercial sources. They suit pricing, invoicing, reporting, and display. They are not suitable for trading, for executing foreign exchange transactions, or for any other forex operation.
Historical rates are one closing set per UTC day. Days recorded since launch cover every currency we serve. Earlier days, back to 4 January 1999, are the euro foreign exchange reference rates published by the European Central Bank (about 30 currencies, on TARGET working days), converted to a USD base. Source: European Central Bank; the reference rates are also available free of charge from the ECB.
The currencies page lists every code with its rate per 1 USD and a rate page for each currency.
Supported currencies
Search the currencies available in the latest snapshot.
Loading currencies…
Webhooks
A webhook is a signed POST to an HTTPS address you choose. Create one in Webhooks; Starter allows 1, Pro 3, and Business 5. The free plan has no webhooks. Deliveries don't count against your monthly quota. After a downgrade, only your oldest enabled webhooks up to the new limit receive deliveries.
When a delivery is sent
A delivery is sent when the edition for your webhook changes, which depends on your plan's update interval (see Plans and limits). A webhook follows the base currency and currencies it was created with. Its first delivery is the next change after it is created, so use Send test in the console to check an endpoint right away.
Headers
| Header | Value | Notes |
|---|---|---|
| Content-Type | application/json | The body is JSON. |
| User-Agent | CurrencyExchangeRate-API-Webhooks/1.0 | Fixed value. |
| X-Webhook-Id | Webhook id | Identifies the webhook. |
| X-Webhook-Delivery | Delivery id | Same on every retry, so you can ignore duplicates. |
| X-Webhook-Event | rates.updated or webhook.test | The event type. |
| X-Webhook-Signature | t=<unix seconds>,v1=<hex> | HMAC-SHA256 signature. See Verify signatures below. |
Bodies
rates.updated carries the edition for your base and currencies. The id matches the X-Webhook-Delivery header.
rates.updated
JSON{ "id": "<delivery id>", "type": "rates.updated", "createdAt": "2026-10-08T00:00:31.000Z", "data": { "base": "EGP", "updatedAt": "2026-10-08T00:00:01.000Z", "nextUpdateAt": "2026-10-09T00:00:01.000Z", "rates": { "EUR": 0.01895679012, "USD": 0.02057613169 } }}webhook.test
JSON{ "id": "<delivery id>", "type": "webhook.test", "createdAt": "2026-10-08T00:00:31.000Z", "data": { "message": "This is a test delivery from CurrencyExchangeRate-API.com." }}Verify signatures
The header is t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<raw body>, keyed with the webhook secret. Check the signature against the raw bytes you received, before you parse the JSON. Compare with a constant-time function, and reject a t more than 5 minutes old. The secret starts with whsec_ and is shown once, when you create or rotate the webhook.
Verify a delivery
JavaScript · Node.jsimport { createHmac, timingSafeEqual } from 'node:crypto'; const MAX_AGE_SECONDS = 5 * 60; // rawBody: the exact request body as a string, before JSON.parse.// header: the X-Webhook-Signature value. secret: your whsec_ secret.export function verifyWebhook(rawBody, header, secret) { const fields = Object.fromEntries( header.split(',').map((part) => part.split('=')), ); const age = Math.floor(Date.now() / 1000) - Number(fields.t); if (!Number.isFinite(age) || age > MAX_AGE_SECONDS) return false; const expected = createHmac('sha256', secret) .update(`${fields.t}.${rawBody}`) .digest('hex'); const received = Buffer.from(fields.v1 ?? '', 'utf8'); const actual = Buffer.from(expected, 'utf8'); return ( received.length === actual.length && timingSafeEqual(received, actual) );}Retries
Your endpoint has 10 seconds to answer. Any 2xx response counts as delivered; a timeout, a connection error, a non-2xx response, or a redirect counts as a failed attempt. Failed rates.updated deliveries are retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, and 12 hours: 8 attempts in total. Retries reuse the delivery id and body, with a fresh t and signature. After the eighth failed attempt the delivery is marked failed.
If three deliveries in a row fail all eight attempts, the webhook is disabled automatically. Fix the endpoint, then enable the webhook again in the console.
Endpoint address
- Use an absolute
https://URL of at most 2048 characters. Query strings are allowed. - The URL can't contain a username, a password, or a
#fragment. - The host must be public.
localhostand private, loopback, link-local, and other reserved addresses are refused, both when you save the URL and on every send.
Streaming
A WebSocket connection receives a rates message whenever the edition for its base and symbols changes, so you don't have to poll. Streaming is available on paid plans, with the same API keys as the REST endpoints.
Connect
Stream URL
WebSocketwss://api.currencyexchangerate-api.com/api/v1/stream?base=EGP&symbols=EUR,USDThe optional query takes the same base and symbols as /latest. Without them, the connection uses USD and every currency. Authenticate with one of the two methods below. Keys never go in the URL.
- Server clients send
Authorization: Bearer YOUR_API_KEYon the upgrade request. - Browsers send a first message,
{"type":"auth","key":"YOUR_API_KEY"}, within 10 seconds. Without it, the server closes the connection with4408. Keys in browser code are visible to every visitor, so prefer a server-side relay for production.
Messages
| Message | Direction | Meaning |
|---|---|---|
| rates | Server to client | The current edition for this connection. Sent after authentication, after each subscribe, and whenever the edition changes. |
| error | Server to client | A code such as RATES_UNAVAILABLE (no snapshot yet; the connection stays open) or RATES_UNSUPPORTED_CURRENCY. |
| pong | Server to client | Answer to a ping. |
| auth | Client to server | Sends the key as the first message. Only valid once. |
| subscribe | Client to server | Replaces the base and symbols filter. Omit symbols for every currency. The server answers with a rates message. |
| ping | Client to server | Gets a pong. Optional; the server pings you every 30 seconds. |
Every message is a JSON text frame, for example {"type":"rates","data":{"base":"EGP","updatedAt":"…","nextUpdateAt":"…","rates":{"EUR":0.01895679012}}}. A malformed message or an unknown type gets an error with RATES_INVALID_INPUT, and the connection stays open.
Browser example
JavaScriptconst socket = new WebSocket('wss://api.currencyexchangerate-api.com/api/v1/stream?base=EGP&symbols=EUR,USD'); socket.addEventListener('open', () => { // Browsers can't set headers, so send the key as the first message. socket.send(JSON.stringify({ type: 'auth', key: 'YOUR_API_KEY' }));}); socket.addEventListener('message', (event) => { const message = JSON.parse(event.data); if (message.type === 'rates') console.log(message.data.rates); if (message.type === 'error') console.error(message.code);});Limits
- Your plan sets how many connections you can hold across all of your keys: Starter 1, Pro 3, Business 5. One more closes with
4409. The free plan has no stream and closes with4403. - Each connection may send 30 messages in any 60 seconds. More closes it with
1008. - Connecting counts as one request against your quota. Pushed rates don't count. Each connection re-checks its key and plan every 5 minutes, so a revoked key closes with
4401.
Close codes
| Code | Reason | Meaning |
|---|---|---|
| 1001 | — | The server is shutting down. |
| 1008 | — | Too many client messages. |
| 1011 | RATES_INTERNAL_ERROR | Unexpected failure. |
| 4401 | API_KEY_INVALID | Missing, unknown, or revoked key. |
| 4403 | RATES_PLAN_UPGRADE_REQUIRED | Your plan has no WebSocket stream. |
| 4408 | RATES_STREAM_AUTH_TIMEOUT | No auth message within 10 seconds. |
| 4409 | RATES_STREAM_LIMIT_REACHED | Your plan’s connections are all open. |
| 4422 | RATES_INVALID_INPUT or RATES_UNSUPPORTED_CURRENCY | Bad base or symbols in the query. |
| 4429 | API_RATE_LIMITED or API_QUOTA_EXCEEDED | A limit from API access. |