Every currency exchange rates API returns roughly the same information: a base currency, some target currencies, numbers and a time. Each one wraps it differently. Field names change, timestamps switch between Unix seconds and date strings, and one provider glues the currency codes together into a single key. If you ever switch providers or use two at once, these differences are where bugs come from.

This guide puts six response shapes side by side, then builds a small JavaScript adapter that normalises them into one format.

How these were checked. Frankfurter and AllRatesToday shapes come from live calls on 11 November 2026. The others come from each vendor's own documentation examples at the time of writing. Example values are the vendors' own and are not current rates.

Open Exchange Rates

GET https://openexchangerates.org/api/latest.json?app_id=YOUR_APP_ID

{ "disclaimer": "https://openexchangerates.org/terms/", "license": "https://openexchangerates.org/license/", "timestamp": 1449877801, "base": "USD", "rates": { "AED": 3.672538, "AFN": 66.809999, "ALL": 125.716501 } } 

A rates object keyed by currency code, a base, and timestamp in Unix seconds. The free plan is USD base only.

Fixer

GET http://data.fixer.io/api/latest?access_key=YOUR_KEY

{ "success": true, "timestamp": 1519296206, "base": "EUR", "date": "2025-12-13", "rates": { "USD": 1.23396, "GBP": 0.882047, "JPY": 132.360679 } } 

Close to Open Exchange Rates, plus a success flag and a date string. The default base is EUR, and switching base is a paid feature.

currencylayer

GET http://api.currencylayer.com/live?access_key=YOUR_KEY&source=GBP

{ "success": true, "timestamp": 1430401802, "source": "GBP", "quotes": { "GBPUSD": 1.53053, "GBPAUD": 1.94371, "GBPCAD": 1.83758 } } 

This is the odd one. The base is called source, the map is called quotes, and each key is the source code and target code joined: GBPUSD, or USDEUR with a USD source.

ExchangeRate-API

GET https://v6.exchangerate-api.com/v6/YOUR_KEY/latest/USD

{ "result": "success", "time_last_update_unix": 1585267200, "time_last_update_utc": "Fri, 27 Mar 2020 00:00:00 +0000", "time_next_update_unix": 1585353700, "base_code": "USD", "conversion_rates": { "USD": 1, "AUD": 1.4817, "EUR": 0.9013, "GBP": 0.7679 } } 

The documentation, terms and next-update UTC fields are trimmed here. The map is conversion_rates, the base is base_code, and success is a string result rather than a boolean.

Frankfurter

GET https://api.frankfurter.dev/v1/latest?base=USD&symbols=EUR,GBP

{"amount":1.0,"base":"USD","date":"2026-11-10","rates":{"EUR":0.89174,"GBP":0.74501}} 

The simplest shape. amount is the amount converted, date is the reference date of the data, and there is no time of day because the data is daily.

AllRatesToday

GET https://allratestoday.com/api/v1/rates?source=USD&target=EUR,GBP with Authorization: Bearer YOUR_API_KEY

[ {"rate":0.9215,"source":"USD","target":"EUR","time":"2026-04-03T12:00:00Z"}, {"rate":0.7890,"source":"USD","target":"GBP","time":"2026-04-03T12:00:00Z"} ] 

An array of flat records, one per pair, each with its own ISO 8601 time. The shape is the same for one pair or many, so read rates[0].rate, not rates.rate. All targets in one call cost one request.

Currency exchange rates API formats side by side

Provider Base field Rates container Key format Time field Success signal
Open Exchange Rates base rates object EUR timestamp (Unix s) HTTP status; error: true on failure
Fixer base rates object EUR timestamp (Unix s), date success boolean
currencylayer source quotes object USDEUR timestamp (Unix s) success boolean
ExchangeRate-API base_code conversion_rates object EUR time_last_update_unix result: "success"
Frankfurter base rates object EUR date (YYYY-MM-DD) HTTP status
AllRatesToday source per item top-level array target per item time (ISO 8601) per item HTTP status

Error bodies differ too. Fixer and currencylayer return "success": false with an error object holding code, type and info. Open Exchange Rates returns "error": true with status, message and description. ExchangeRate-API returns "result": "error" with an error-type. Check both the HTTP status and the body flag.

Writing an adapter layer

Pick one internal shape and convert everything into it at the edge. A flat record per pair is the easiest to work with, because it carries its own time and survives being put in a list, a cache or a database row.

// Internal shape: { base, quote, rate, time: Date, provider } const fromUnix = (s) => new Date(s * 1000); const fromDate = (d) => new Date(`${d}T00:00:00Z`); function fromMap(provider, base, map, time) { return Object.entries(map) .filter(([quote]) => quote !== base) .map(([quote, rate]) => ({ base, quote, rate, time, provider })); } const adapters = { openexchangerates(body) { if (body.error) throw new Error(body.message); return fromMap("openexchangerates", body.base, body.rates, fromUnix(body.timestamp)); }, fixer(body) { if (!body.success) throw new Error(body.error?.info ?? "Fixer error"); return fromMap("fixer", body.base, body.rates, fromUnix(body.timestamp)); }, currencylayer(body) { if (!body.success) throw new Error(body.error?.info ?? "currencylayer error"); const base = body.source; const rates = Object.fromEntries( Object.entries(body.quotes).map(([pair, rate]) => [pair.slice(base.length), rate]) ); return fromMap("currencylayer", base, rates, fromUnix(body.timestamp)); }, exchangerateapi(body) { if (body.result !== "success") throw new Error(body["error-type"]); return fromMap("exchangerateapi", body.base_code, body.conversion_rates, fromUnix(body.time_last_update_unix)); }, frankfurter(body) { // rates are for `amount` units of base; divide back to 1 unit const perUnit = Object.fromEntries( Object.entries(body.rates).map(([q, r]) => [q, r / body.amount]) ); return fromMap("frankfurter", body.base, perUnit, fromDate(body.date)); }, allratestoday(body) { if (!Array.isArray(body)) throw new Error("Expected an array"); return body.map((r) => ({ base: r.source, quote: r.target, rate: r.rate, time: new Date(r.time), provider: "allratestoday", })); }, }; export function normalise(provider, body) { const adapt = adapters[provider]; if (!adapt) throw new Error(`No adapter for ${provider}`); return adapt(body); } 

Usage:

const res = await fetch( "https://allratestoday.com/api/v1/rates?source=USD&target=EUR,GBP", { headers: { Authorization: `Bearer ${process.env.ALLRATES_API_KEY}` } } ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const rows = normalise("allratestoday", await res.json()); // [{ base: "USD", quote: "EUR", rate: 0.9215, time: Date, provider: "allratestoday" }, ...] 

Details the adapter has to get right

  • Base mismatch. Open Exchange Rates on the free plan only gives USD base. If your app needs EUR base, derive it: rate(EUR→X) = rate(USD→X) / rate(USD→EUR). See how to handle base currency conversion.
  • Self rates. ExchangeRate-API includes the base itself at 1. The adapter drops it so you do not store USD/USD.
  • Time precision. A Frankfurter date is a day, not a moment. Keep the provider name with the record so later code knows whether time is exact.
  • Numbers. All six return JSON numbers. Keep them as numbers until display, and round only at the last step.
  • Unknown fields. Ignore them. Vendors add fields such as disclaimer or terms_of_use without notice.

When you do not need an adapter

If you are moving off a single provider rather than running several, a compatibility endpoint is less work than an adapter. The migration guides cover Fixer, exchangeratesapi.io, exchangerate.host, fawazahmed0 currency-api and money.js, several with drop-in response shapes. To compare the providers on limits and data rather than JSON, see the comparison pages and the free currency API comparison.

FAQ

Why does currencylayer return keys like USDEUR?

currencylayer puts quotes under a quotes object keyed by source plus target, such as USDEUR or GBPUSD. Strip the first three letters to get the target code.

Which exchange rate APIs return the rate time as a Unix timestamp?

Open Exchange Rates, Fixer and currencylayer return a timestamp field in Unix seconds. ExchangeRate-API returns time_last_update_unix and time_last_update_utc. Frankfurter returns a date string. AllRatesToday returns an ISO 8601 time string per pair.

Why does the AllRatesToday API return an array?

Each element carries its own source, target, rate and time, so one pair and many pairs have the same shape. Read rates[0].rate for a single pair, not rates.rate.

Do error responses also differ between providers?

Yes. Fixer and currencylayer return success false with an error object. Open Exchange Rates returns error true with status and message. ExchangeRate-API returns result error with an error-type field. Check the HTTP status and the body flag in your adapter.

Is there a drop-in way to switch providers without an adapter?

AllRatesToday has compatibility endpoints and migration guides for several providers on the /migrate/ page. An adapter layer is still the cleanest option if you want to support more than one provider at once.

Related

Originally published at allratestoday.com.