Any free currency conversion API will give you a rate. Turning that rate into a correct amount of money is your job, and it is where most bugs live: floats that drift, yen with cents, three-decimal dinars cut to two, and rounded rates saved to the database. This tutorial covers the rules and shows working code in JavaScript and Python.
The six rules
- Never round the rate. Multiply with the full rate the API returned.
- Convert, then round once. Round the final amount, not intermediate steps.
- Round to the target currency's minor units. JPY has 0, USD has 2, KWD has 3.
- Pick one rounding mode and use it everywhere. Half-even unless a rule says otherwise.
- Do the arithmetic in integers or decimals, not binary floats.
- Store the rate and its time next to the converted amount.
Minor units: not every currency has cents
ISO 4217 defines, for each currency, how many digits follow the decimal point. Examples from the current ISO list:
| Currency | Code | Minor units | 1,000 USD at an example rate |
|---|---|---|---|
| US dollar | USD | 2 | 1,000.00 |
| Euro | EUR | 2 | 923.40 at 0.9234 |
| Japanese yen | JPY | 0 | 153,470 at 153.47 |
| Kuwaiti dinar | KWD | 3 | 307.100 at 0.3071 |
| Bahraini dinar | BHD | 3 | — |
| Chilean Unidad de Fomento | CLF | 4 | — |
Showing ¥153,470.00 is wrong, and rounding KWD to two decimals loses a real unit (the fils). Get the digits from the currency, not from a constant 2.
One trap: display libraries do not always match ISO. In Node 22, Intl.NumberFormat shows IQD, HUF and IDR with 0 fraction digits, while the ISO list gives IQD 3 and HUF and IDR 2. That is fine for display. For stored amounts, use the ISO figure. The ISO 4217 metadata guide shows where to get the full list.
Rounding modes
When the digit after the cut is exactly 5, you need a rule:
| Value | Half-up | Half-even (banker's) |
|---|---|---|
| 2.125 | 2.13 | 2.12 |
| 2.135 | 2.14 | 2.14 |
| 230.375 | 230.38 | 230.38 |
Half-up always rounds .5 away from zero, so over thousands of amounts it adds a small upward bias. Half-even rounds .5 to the nearest even digit, so ups and downs cancel out. Many finance systems use half-even. Some tax and invoicing rules require half-up, and payment providers have their own rules. Pick the one your rule requires, write it down, and use it in every service.
Why floats are not enough
JavaScript numbers and Python float are binary. Many decimal values cannot be stored exactly:
0.1 + 0.2 // 0.30000000000000004 (1.005).toFixed(2) // "1.00", because 1.005 is stored as 1.00499999... For a single display this rarely matters. For totals, refunds and reconciliations it does. Work in integer minor units (cents, fils, yen) or a decimal type.
JavaScript: integer minor units with BigInt
Fetch the rate, then do the multiplication in BigInt, then round half-even:
const res = await fetch( "https://allratestoday.com/api/v1/rates?source=USD&target=EUR,JPY,KWD", { headers: { Authorization: `Bearer ${process.env.ALLRATES_API_KEY}` } } ); if (!res.ok) throw new Error(`HTTP ${res.status}`); const rows = await res.json(); // array const rates = Object.fromEntries(rows.map((r) => [r.target, r])); // ISO 4217 minor units for the currencies you support const MINOR = { USD: 2, EUR: 2, JPY: 0, KWD: 3, BHD: 3 }; function divRoundHalfEven(num, den) { const neg = (num < 0n) !== (den < 0n); let n = num < 0n ? -num : num; const d = den < 0n ? -den : den; let q = n / d; const r2 = (n % d) * 2n; if (r2 > d || (r2 === d && q % 2n === 1n)) q += 1n; return neg ? -q : q; } // amountMinor: BigInt in the source currency's minor units (e.g. cents) function convertMinor(amountMinor, from, to, rate) { const s = String(rate); if (s.includes("e")) throw new Error("Use a decimal library for very small rates"); const [int, frac = ""] = s.split("."); const rateScaled = BigInt(int + frac); // rate * 10^frac.length const num = amountMinor * rateScaled * 10n ** BigInt(MINOR[to]); const den = 10n ** BigInt(MINOR[from] + frac.length); return divRoundHalfEven(num, den); // target minor units } const usd = 123456n; // $1,234.56 const eurMinor = convertMinor(usd, "USD", "EUR", rates.EUR.rate); With an example rate of 0.9215, $1,234.56 becomes 113765 cents, or €1,137.65. The exact product is 1,137.64704; rounding happens once, at the end.
To display it, divide by the minor unit and let Intl.NumberFormat handle symbols and grouping:
function format(minor, currency, locale = "en-US") { const digits = MINOR[currency]; return new Intl.NumberFormat(locale, { style: "currency", currency, minimumFractionDigits: digits, maximumFractionDigits: digits, }).format(Number(minor) / 10 ** digits); } format(eurMinor, "EUR", "de-DE"); // "1.137,65 €" The amount is already rounded in integers, so the Number conversion here is only for display and is exact for normal amounts. If you round inside Intl.NumberFormat instead, pass roundingMode: "halfEven", which Node 22 and current browsers support.
Python: Decimal
Python's decimal module does this directly. Parse the JSON numbers as Decimal so the rate keeps the exact digits the API sent:
import os from decimal import Decimal, ROUND_HALF_EVEN import requests MINOR = {"USD": 2, "EUR": 2, "JPY": 0, "KWD": 3, "BHD": 3} resp = requests.get( "https://allratestoday.com/api/v1/rates", params={"source": "USD", "target": "EUR,JPY,KWD"}, headers={"Authorization": f"Bearer {os.environ['ALLRATES_API_KEY']}"}, timeout=10, ) resp.raise_for_status() rows = resp.json(parse_float=Decimal) # list of dicts, rates as Decimal rates = {r["target"]: r for r in rows} def convert(amount: Decimal, to: str, rate: Decimal) -> Decimal: step = Decimal(1).scaleb(-MINOR[to]) # 0.01, 1, 0.001 ... return (amount * rate).quantize(step, rounding=ROUND_HALF_EVEN) amount = Decimal("1234.56") eur = convert(amount, "EUR", rates["EUR"]["rate"]) jpy = convert(amount, "JPY", rates["JPY"]["rate"]) Never build a Decimal from a float (Decimal(0.1) is 0.1000000000000000055511151231257827...). Build it from a string or from parse_float=Decimal.
Line items and totals
A cart with three items raises a question: convert each line and add them up, or add them up and convert the total? The two can differ by a minor unit or more. With an example rate of 0.9215, three items of $9.99 convert to €9.21 each, €27.63 in total. Converting the $29.97 total gives €27.62.
Neither is wrong, but you must pick one and show it consistently:
- Convert each line when the customer sees per-item prices in their currency. The total is then the sum of what they saw.
- Convert the total when only the total is shown, or when a payment provider converts the charge as one amount.
- If you must split a converted total back into lines (for refunds or tax), allocate the leftover minor units to the largest lines so the parts add up exactly to the total.
Store the rate and the time, not just the result
When a customer asks why they paid €1,137.65, "it was the rate" is not an answer. Store:
| Column | Example |
|---|---|
amount_source | 1234.56 |
currency_source | USD |
amount_target | 1137.65 |
currency_target | EUR |
rate | 0.9215 (full precision, as received) |
rate_time | 2026-04-03T12:00:00Z |
rate_type | mid-market |
rounding | half-even |
With this row you can reproduce the amount exactly, years later. Never store a rate rounded to 2 or 4 decimals "for display". Round in the view.
If the amount is for an invoice, VAT or customs, the rule usually names an official rate and a date. Use that official rate, and store its institution and rate date in the same row. The central bank rates API serves those tables.
Inverse rates and round trips
The inverse of USD/EUR 0.9215 is 1 ÷ 0.9215 = 1.085187... For display, show "1 EUR = 1.0852 USD". For calculation, use the full value, or better, multiply by the original rate in the other direction.
Two things are not true:
- A second API call for EUR/USD will not always equal 1 ÷ USD/EUR. It is a separate snapshot and may come from a different minute.
- Converting back does not give the original amount. ¥1,000 to USD at 0.006516 is $6.52. Back to yen at 153.47 is ¥1,001. Each step rounds to minor units, and the errors do not cancel.
So keep the original amount and currency as the source of truth. Refund in the original currency where possible, and do not chain conversions through a base currency when you already have the direct rate.
The convert endpoint
On the Medium plan and above, AllRatesToday can do the multiplication for you. Add amount to the rates call with exactly one target:
curl "https://allratestoday.com/api/v1/rates?source=USD&target=EUR&amount=1000" \ -H "Authorization: Bearer YOUR_API_KEY" {"from":{"currency":"USD","amount":1000},"to":{"currency":"EUR","amount":923.4},"rate":0.9234,"source":"mid-market"} to.amount is rounded to 6 decimal places, not to the currency's minor units, and rate is full precision. You still apply your own rounding mode and minor units before you charge or store anything. On other plans, multiply the rate from the same endpoint as shown above.
For more on choosing an API, see the free currency API comparison and the JavaScript converter guide.
FAQ
How many decimal places should I keep for an exchange rate?
Keep whatever the API returns. Do not round the rate before multiplying. Round only the converted amount, to the minor units of the target currency.
Should I use banker's rounding for currency conversion?
Round half to even (banker's rounding) avoids an upward bias when you round many amounts, which is why it is common in finance. Some invoicing, tax or payment rules require half-up instead. Use what your rule or your payment provider specifies, and use it everywhere.
How many decimals does JPY or KWD have?
ISO 4217 gives JPY 0 minor units, KWD and BHD 3, and most currencies 2. Display libraries sometimes differ from ISO for a few currencies, so use the ISO list for amounts you store.
Why does converting back give a different amount?
Each conversion rounds to the target currency's minor units, and the two directions use different rates. The rounding errors do not cancel. Keep the original amount and never convert back and forth.
Is the inverse rate exactly 1 divided by the rate?
Mathematically yes, for the same snapshot. In practice the result has more digits than the original rate, and a separate request for the reverse pair is a separate snapshot. Compute the inverse from full precision and round only for display.
Rates with full precision. Mid-market rates for 160+ currencies refreshed every 60 seconds, each with a timestamp you can store next to the amount. Start with a free API key. Get a free API key
Originally published at allratestoday.com.