# Migrate an Existing Currency API Integration

> Move a currency API integration to CurrencyCore: plan choice, bearer auth, currency and history coverage, response mapping, endpoint mapping, and a cutover checklist.

Canonical: https://currency-core.com/docs/guides/migration/
Updated: 2026-11-12
Published: 2026-11-11

---
This guide is the provider-neutral path. It covers the decisions that apply
whichever currency API you are leaving: which plan to start on, how
authentication changes, what currency and history coverage you actually get,
how to map response fields, and how to cut over without guessing.

Provider-specific mappings are in the [Fixer guide](/docs/guides/migrate-fixer/),
the [CurrencyLayer guide](/docs/guides/migrate-currencylayer/), and the
[ExchangeRate-API guide](/docs/guides/migrate-exchangerate-api/). All integration
guides are listed under [Guides](/docs/guides/).

CurrencyCore is **not a drop-in replacement at the wire level**. The host, the
auth header, the response envelope, and in some cases the base-currency
semantics change. The ISO codes and the arithmetic do not. Plan for response
mapping and a comparison pass, not only a host swap.

## Provider-specific mappings

Each provider guide covers the same six areas: auth, base semantics, response
shape, history, errors and cutover. Start with the one you are leaving.

| Provider | Auth style | Main response shape | What changes most | Guide |
|---|---|---|---|---|
| Fixer | `access_key` query parameter | `latest` and `convert` objects | Move the key to a bearer header and map the result envelope | [Fixer](/docs/guides/migrate-fixer/) |
| CurrencyLayer | `access_key` query parameter | `quotes` keyed by concatenated pair | Strip the base prefix and map to `rates` | [CurrencyLayer](/docs/guides/migrate-currencylayer/) |
| ExchangeRate-API | key in the URL path | `conversion_rates` map | Move the key out of the URL and drop the status envelope | [ExchangeRate-API](/docs/guides/migrate-exchangerate-api/) |
| Open Exchange Rates | `app_id` query parameter | `rates` map with an epoch timestamp | Use header auth and a UTC calendar date | [Open Exchange Rates](/docs/guides/migrate-open-exchange-rates/) |
| currencyapi.com | `apikey` header or query | `data` wrapper | Unwrap `data` and use a bearer header | [currencyapi.com](/docs/guides/migrate-currencyapi/) |
| Frankfurter | keyless | `rates` map | Add auth, a base path and plan limits | [Frankfurter](/docs/guides/migrate-frankfurter/) |

If your provider is not listed, use the provider-neutral mapping below. The
decisions are the same.

## 1. Choose the entry plan

Public CurrencyCore pricing was checked on **2026-11-10**.

| Stage | Plan | What it includes | Main limits |
|---|---|---|---|
| Evaluation | Free, $0 | 1,000 requests per month, 100 AI credits, 3 seats | 60 requests/min, last 30 days of history, test keys only |
| Production entry | Starter, $9/mo | 50,000 requests per month, full history, live keys, 10 seats | 120 requests/min, no analytics endpoints |
| Analytics and BYOK | Growth, $29/mo | 500,000 requests per month, analytics endpoints, BYOK | 600 requests/min |
| High volume | Business, $199/mo | 10,000,000 requests per month, SSO and audit logs | 6,000 requests/min |

Use Free to verify codes and response shapes. Use Starter when the integration
needs live keys or history older than 30 days. Use Growth only when you need
`/v1/history/analysis`, `/v1/volatility`, `/v1/correlation`, `/v1/drawdown`,
`/v1/safe-haven`, `/v1/mean-reversion`, or `/v1/ppp/analysis`. See
[Rate limits and plan quotas](/docs/rate-limits/) for the full table.

## 2. Move authentication out of the URL

Many providers take a key as a query parameter named `access_key`, `apikey`, or
`key`. CurrencyCore takes a header:

```bash
curl "https://api.currency-core.com/v1/rates/USD" \
  -H "Authorization: Bearer cc_live_your_key"
```

`x-api-key: cc_live_your_key` is equivalent. There is no query-string
authentication. Remove the old query parameter from code, fixtures, logs,
browser calls, and shared dashboards. A missing or invalid key returns
`401 unauthorized`.

Test keys use the `cc_test_` prefix and always run at the Free per-minute limit.
Live keys use `cc_live_` and require a paid plan. Both count against the same
organization quota. See [Authentication](/docs/authentication/) for key
creation, rotation, and environment behavior.

## 3. Diff the currency set before you switch

CurrencyCore quotes **160 fiat currencies**. `GET /v1/currencies` returns **165
entries**: the 160 fiat codes plus `XAU`, `XAG`, `XPT`, `XPD`, and `XDR` as
reference entries. The rate feed is fiat only, so those five entries are not
quoted rates.

Three practical consequences:

- A provider that advertises more than 160 currencies may include metals,
  crypto, or regional codes that CurrencyCore does not quote. Check every code
  you store against [`/v1/currencies`](/docs/currencies/), not against the
  provider's headline count.
- There are no cryptocurrency rates. If your product prices BTC, ETH, or a
  stablecoin, keep a separate crypto provider.
- Precious metals are not in the rate feed. If your product needs `XAU` or
  `XAG`, keep a metals provider or stay with a vendor that includes them.

Do the diff in code, not by reading a marketing page. Store the result in a
fixture and fail a test if a required code disappears.

## 4. Understand history coverage and cadence

CurrencyCore history is not one uniform daily series from 1949. Coverage depends
on the pair:

| Series | Coverage |
|---|---|
| GBP/USD and USD/INR annual parity | 1949 |
| Daily major pairs | 1971 |
| EUR pairs | 1999 |

Plan access is separate from data depth:

- **Free** can request the last **30 days**. Older dates return `403 forbidden`.
- **Starter and above** can request the full dataset.

The update model is also different from a tick feed. CurrencyCore uses
central-bank reference rates, polled hourly, with daily fixings underneath.
There is **no real-time quote** and **no 60-second update guarantee**. If your
product needs executable sub-minute prices, CurrencyCore is the wrong feed.
Dates are UTC calendar dates.

When a requested date has no snapshot, the API resolves to the most recent
earlier one. `date` is the date actually used; `requestedDate` appears only when
it differs. `asOf` lists currencies whose rate was carried forward. Your adapter
should preserve those fields instead of inventing a fresh timestamp.

## 5. Map response shapes

The provider-neutral mapping is small, but it is not zero.

### Rate snapshot

A snapshot with a rates map maps directly:

```json
{ "base": "USD", "date": "2026-11-10", "rates": { "EUR": 0.9214, "INR": 84.07 } }
```

A provider that keys rates by concatenated pairs needs one transformation:

```json
{ "source": "USD", "quotes": { "USDEUR": 0.9214, "USDINR": 84.07 } }
```

becomes `rates.EUR` and `rates.INR` when the base is `USD`. Strip the six-letter
prefix, not a fixed three-letter chunk, so codes of different lengths do not
break parsing.

### Conversion

A single-result envelope becomes an array entry:

```json
{ "result": 92.14, "rate": 0.9214 }
```

becomes:

```json
{ "results": [{ "to": "EUR", "result": 92.14, "rate": 0.9214 }] }
```

Read `results[0].result` for the converted amount. Read `results[0].rate` for
the rate. If you send multiple targets in `to`, match each result by `to`
instead of assuming array position.

### History

A provider that returns a map of dates to currency maps needs flattening:

```json
{ "rates": { "2026-11-09": { "EUR": 0.92 }, "2026-11-10": { "EUR": 0.9214 } } }
```

becomes a dense date-to-rate map for one currency:

```json
{ "currency": "EUR", "base": "USD", "rates": { "2026-11-09": 0.92, "2026-11-10": 0.9214 } }
```

CurrencyCore fills weekends and holidays by carrying the previous rate forward.
That is deliberate. If your chart expects missing points, filter them in your
adapter instead of treating the dense series as an error.

### Field mapping

| Common provider field | CurrencyCore field | Note |
|---|---|---|
| `base` or `source` | `base` | `/v1/rates` is always USD; use `/v1/rates/{base}` for another base |
| `date`, `timestamp`, or `time_last_update_utc` | `date` | CurrencyCore returns a UTC calendar date, not an epoch |
| `rates` | `rates` | Values are units of the target per 1 unit of base |
| `result` or `conversion_result` | `results[0].result` | Conversion responses use an array |
| `quotes.USDEUR` | `rates.EUR` | Strip the base prefix |
| `start_date` and `end_date` | `from` and `to` | Inclusive UTC dates |
| `symbols` or `codes` | `/v1/currencies` | CurrencyCore returns metadata, not only a code list |
| `success: true` | HTTP `200` | CurrencyCore signals failure with an HTTP status and an error envelope |

Errors are uniform:

```json
{ "error": { "code": "unknown_currency", "message": "Currency code 'XYZ' is not recognized." } }
```

Read the stable `code`, not the message text. See [Errors](/docs/errors/) for the
full catalog and retry guidance.

## 6. Map endpoints

| Common provider endpoint | CurrencyCore endpoint | Caveat |
|---|---|---|
| Latest rates | `/v1/rates` | USD base only |
| Latest rates with another base | `/v1/rates/{base}` | Base moves from query to path |
| Convert an amount | `/v1/convert` | Response uses `results[]`, up to 25 targets |
| Dated snapshot | `/v1/rates/{base}?date=YYYY-MM-DD` | Free is limited to 30 days |
| Dated conversion | `/v1/convert?date=YYYY-MM-DD` | Same Free window |
| Time series | `/v1/history` | One currency per call, dense series |
| Change or fluctuation | `/v1/history/analysis` | Growth or higher, and not a field-for-field match |
| Symbols or supported codes | `/v1/currencies` | Public, no key, no quota |
| Country reference | `/v1/countries` | Public, no key, no quota |
| PPP conversion | `/v1/convert?ppp=true` | No equivalent in most FX-only providers |
| Natural-language query | `/v1/ai` | Separate AI credit pool |

`/v1/rates` and `/v1/convert` cover the core migration. Everything else is
optional and can be adopted later. The full parameter and response reference is
at [API reference](/docs/api/) and in the [OpenAPI specification](/docs/openapi/).

## 7. Cut over in a controlled order

1. **Inventory calls.** List every provider endpoint, base currency, date range,
   and consumer. Mark which calls need history and which only need the latest
   snapshot.
2. **Create the key.** Use a test key on Free for the first pass. Move to a live
   key only when the account is on a paid plan.
3. **Diff currencies.** Compare every stored code with `/v1/currencies`. Decide
   what happens to unsupported codes before traffic moves.
4. **Write the adapter.** Map auth, base currency, date fields, `rates`,
   `results[]`, and the error envelope. Keep the old provider client behind the
   same interface so rollback is a switch, not a rewrite.
5. **Run shadow traffic.** Send the same logical request to both providers. Do
   not compare timestamps literally; compare the rate for the same fixing date.
6. **Compare with tolerances.** Allow for provider-specific rounding and for
   carry-forward dates. Investigate every gap beyond your tolerance instead of
   averaging it away.
7. **Switch reads.** Move low-risk read paths first. Keep writes and billing
   calculations on the old provider until the comparison looks stable.
8. **Watch the right errors.** `401` is auth, `402` is billing, `403` is a plan
   or date gate, and `429` is a quota or per-minute limit. Honor `Retry-After`.
9. **Revoke the old key.** Do not leave dual auth in production. Rotate or
   revoke the provider key once the new path has served real traffic.
10. **Record rollback.** Keep the old adapter and the last known good config
    until the next billing cycle, even after cutover.

## Caveats to decide before you start

- **No metals or crypto quotes.** The feed is fiat only.
- **No sub-minute feed.** Reference rates are polled hourly; fixings are daily.
- **Base semantics differ.** `/v1/rates` is USD. Use `/v1/rates/{base}` when
  your old provider defaulted to EUR or another base.
- **History is plan-gated.** Free stops at 30 days even though the underlying
  dataset reaches 1949.
- **Quota accounting is not identical.** Only HTTP 2xx responses count toward
  the monthly quota. The per-minute limit counts every request, including
  errors and retries. See [Rate limits](/docs/rate-limits/).
- **Analytics are not a rename.** Provider "change" or "fluctuation" responses
  are not the same object as CurrencyCore's analytics endpoints.

If these caveats are acceptable, the migration is mostly adapter work and
comparison testing. If any of them is a hard product requirement, keep the
provider that meets it instead of forcing an equivalence that does not exist.

## Related

- [OpenAPI specification](/docs/openapi/)
- [Authentication](/docs/authentication/)
- [Errors](/docs/errors/)
- [Rate limits](/docs/rate-limits/)
- [All integration guides](/docs/guides/)
- [`/convert` endpoint](/docs/convert-endpoint/)
- [`/rates` endpoint](/docs/rates-endpoint/)
- [`/history` endpoint](/docs/history-endpoint/)
