Free Currency Data API: ISO 4217 Codes, Minor Units and Withdrawn Currencies
Rates are only half of currency data. To show a price, validate a form or store an amount, you also need to know that KWD is a real code, that it has three decimal places, that its symbol is not $, and that HRK stopped being a currency in 2023. A free currency data API or file should give you that metadata. This guide covers what the metadata is, where to get it for free, and how to validate user input with it.
What currency metadata includes
ISO 4217 is the standard for currency codes. For each currency, it defines:
| Field | Example | Notes |
|---|---|---|
| Alphabetic code | JPY | Three letters. Usually the country's ISO 3166 code plus a letter. |
| Numeric code | 392 | Three digits. Used in card networks, SWIFT messages and some banking files. |
| Minor units | 0 | Digits after the decimal point. |
| Name | Yen | ISO's English name. |
| Entities | JAPAN | Countries and territories that use it. |
Things ISO does not define, but your app usually needs:
- Symbols such as
$,€,¥. Many currencies share$, so a symbol alone does not identify a currency. - Localized names, such as "Japanischer Yen" in German.
- Formatting rules: where the symbol goes, the decimal separator, grouping.
These come from CLDR, the Unicode locale data, which is what Intl in JavaScript and most i18n libraries use.
Minor units are not always 2
A few examples from the ISO list dated 17 September 2026:
| Code | Currency | Minor units |
|---|---|---|
| USD | US Dollar | 2 |
| JPY | Yen | 0 |
| KRW | Won | 0 |
| KWD | Kuwaiti Dinar | 3 |
| BHD | Bahraini Dinar | 3 |
| IQD | Iraqi Dinar | 3 |
| CLF | Unidad de Fomento | 4 |
| XAU | Gold | N.A. |
Display libraries can disagree with ISO. In Node 22, Intl.NumberFormat uses 0 fraction digits for IQD, HUF and IDR, while ISO gives 3, 2 and 2. CLDR reflects how amounts are usually written; ISO defines the unit. Store amounts using ISO minor units. The rounding and precision guide shows how to use them in calculations.
Codes change: withdrawn and redenominated currencies
Currency lists are not static. ISO keeps a separate historic list (List Three) with a withdrawal month for each retired code. Recent changes you are likely to meet in real data:
| Old code | New code | What happened | ISO withdrawal |
|---|---|---|---|
| HRK (Kuna) | EUR | Croatia adopted the euro on 1 January 2023, at 7.53450 kuna per euro | 2023-01 |
| SLL (Leone) | SLE (Leone) | New leone introduced on 1 July 2022; 1 SLE = 1,000 SLL | 2023-12 |
| VEF (Bolívar) | VES (Bolívar Soberano) | Redenomination in August 2018 | 2018-08 |
| MRO (Ouguiya) | MRU (Ouguiya) | New ouguiya from January 2018; 1 MRU = 10 MRO | 2017-12 |
| ZWL (Zimbabwe Dollar) | ZWG (Zimbabwe Gold) | ZWG introduced in April 2024 | 2024-09 |
| ANG (Netherlands Antillean Guilder) | XCG (Caribbean Guilder) | Curaçao and Sint Maarten moved to the Caribbean guilder | 2025-03 |
Venezuela is a good example of why you should read the list rather than assume. The current ISO list carries two codes for Venezuela, VES (928) and VED (926), both named Bolívar Soberano. VED was added after another redenomination in October 2021. Check which one your rate provider uses before you hard-code either.
What this means for your code:
- Old records keep old codes. An invoice from 2022 in HRK is still an HRK invoice. Do not rewrite it as EUR.
- New input should reject withdrawn codes, or map them with a clear message ("Croatia uses EUR since 2023").
- Redenominations change amounts, not just codes. 1,000,000 SLL is 1,000 SLE. Never relabel an amount without converting it.
- Numeric codes can be reused. The bolívar has had several alphabetic codes; VEF (937) sits in the historic list several times, under slightly different names. Key your data on the alphabetic code plus a date, not on the numeric code alone.
Where to get currency data for free
The ISO 4217 Maintenance Agency
SIX maintains ISO 4217 and offers the current list and the historic list as free XML and Excel downloads. This is the primary source. The XML is easy to parse:
<CcyNtry>
<CtryNm>KUWAIT</CtryNm>
<CcyNm>Kuwaiti Dinar</CcyNm>
<Ccy>KWD</Ccy>
<CcyNbr>414</CcyNbr>
<CcyMnrUnts>3</CcyMnrUnts>
</CcyNtry> The current list has one entry per country and currency, so the same code appears many times (EUR appears once per euro-area country). It also contains fund codes, such as CLF, and special codes: XAU for gold, XDR for the IMF's SDR, XTS for testing and XXX for "no currency". Filter those out if you only want money people pay with.
Download the file in your build or once a month, and commit the parsed result. Do not fetch it on every request.
A currency data API
If you already use a rates API, it often has a list endpoint that tells you which codes it can price. On AllRatesToday:
curl "https://allratestoday.com/api/v1/symbols" \
-H "Authorization: Bearer YOUR_API_KEY" {"currencies":[{"code":"USD","name":"US Dollar","symbol":"$"},{"code":"EUR","name":"Euro","symbol":"€"}]} This is the list you should validate against when the next step is a rate lookup. A code that ISO knows but your provider cannot price is still a code you cannot convert. The full list is on the supported currencies page.
The Intl API in JavaScript
Browsers and Node ship CLDR data:
const codes = Intl.supportedValuesOf("currency"); // ["AED", "AFN", ...]
const names = new Intl.DisplayNames(["en"], { type: "currency" });
names.of("KWD"); // "Kuwaiti Dinar"
new Intl.NumberFormat("en-US", { style: "currency", currency: "KWD" })
.resolvedOptions().maximumFractionDigits; // 3 It is free and offline, and it is good for names and formatting in the user's language. It is not an authoritative list of valid codes. In Node 22, Intl.supportedValuesOf("currency") returned 162 codes. It included withdrawn HRK, SLL and ZWL, and left out VED, MRO and the X codes such as XAU. The result also depends on the runtime's ICU version, so two environments can disagree.
Validating user input
Validate in layers, from cheap to specific:
// 1. Built at startup from /api/v1/symbols (or your committed ISO file)
async function loadCurrencies(apiKey) {
const res = await fetch("https://allratestoday.com/api/v1/symbols", {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { currencies } = await res.json();
return new Map(currencies.map((c) => [c.code, c]));
}
const WITHDRAWN = {
HRK: "Croatia uses EUR since 1 January 2023.",
SLL: "Sierra Leone uses SLE (1 SLE = 1,000 SLL).",
MRO: "Mauritania uses MRU (1 MRU = 10 MRO).",
};
function validateCurrency(input, known) {
const code = String(input).trim().toUpperCase();
if (!/^[A-Z]{3}$/.test(code)) return { ok: false, error: "Use a 3-letter ISO 4217 code." };
if (WITHDRAWN[code]) return { ok: false, error: `${code} is withdrawn. ${WITHDRAWN[code]}` };
if (!known.has(code)) return { ok: false, error: `${code} is not supported.` };
return { ok: true, code, meta: known.get(code) };
} The same in Python:
import os, re, requests
def load_currencies():
r = requests.get(
"https://allratestoday.com/api/v1/symbols",
headers={"Authorization": f"Bearer {os.environ['ALLRATES_API_KEY']}"},
timeout=10,
)
r.raise_for_status()
return {c["code"]: c for c in r.json()["currencies"]}
def validate_currency(value: str, known: dict) -> str:
code = value.strip().upper()
if not re.fullmatch(r"[A-Z]{3}", code):
raise ValueError("Use a 3-letter ISO 4217 code.")
if code not in known:
raise ValueError(f"{code} is not supported.")
return code Some practical points:
- Normalize case before checking. Users type
usd. - Do not accept symbols as input.
$could be USD, CAD, AUD, MXN and many more. - Offer a list, not a text box, wherever you can. Build it from the same data you validate against, with names from
Intl.DisplayNamesin the user's language. - Cache the list. Currencies change a few times a year. Refresh daily or at deploy, not per request.
- Keep withdrawn codes readable. Your database may hold them in old rows. Validate new input strictly, but do not crash on historic data.
Putting it together
A solid setup uses three sources, each for what it is good at:
- ISO 4217 lists for the authoritative code, numeric code, minor units and withdrawal dates.
- Your rate provider's symbols endpoint for which codes you can actually price.
- **
Intl** for localized names and formatting.
If you are still choosing the rate side, the free currency API comparison covers free options. AllRatesToday covers 160+ currencies with mid-market rates refreshed every 60 seconds, and the docs list every endpoint.
FAQ
Where can I get the official ISO 4217 currency list?
From the ISO 4217 Maintenance Agency, which is SIX. It provides the current list (List One) and the list of historic, withdrawn codes (List Three) as XML and Excel files, free to download.
Is HRK still a valid currency code?
No. Croatia adopted the euro on 1 January 2023, and ISO 4217 lists HRK as withdrawn from January 2023. Use EUR for Croatia. Keep HRK only to read old records.
What replaced SLL in Sierra Leone?
SLE, the new leone, introduced on 1 July 2022 at 1 SLE to 1,000 SLL. ISO 4217 lists SLL as withdrawn from December 2023.
Does Intl.supportedValuesOf('currency') return only current ISO codes?
No. In Node 22 it returns 162 codes that include some withdrawn ones, such as HRK, and leave out some current ones, such as VED and the X codes for gold and the SDR. Use it for display, not as your list of valid codes.
How many decimal places does a currency have?
ISO 4217 gives a minor unit count for each currency: 2 for most, 0 for JPY and KRW, 3 for KWD, BHD and IQD. Read it from the ISO list rather than assuming 2.
Currency list and rates from one API
The symbols endpoint lists every currency with its name and symbol, and the rates endpoint covers 160+ currencies refreshed every 60 seconds.
Get a free API key