---
title: "Free Currency Conversion API: Rounding, Precision and Minor Units Done Right"
description: "Using a free currency conversion API correctly: rate precision, ISO 4217 minor units, half-even rounding, convert-then-round, and storing rate and time."
canonical: https://allratestoday.com/blog/currency-conversion-api-rounding-precision/
source: AllRatesToday (https://allratestoday.com/) — Markdown mirror of the page above; index of all pages at https://allratestoday.com/llms.txt
generated: 2026-11-11T14:57:32.457Z
---

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

1. **Never round the rate.** Multiply with the full rate the API returned.
2. **Convert, then round once.** Round the final amount, not intermediate steps.
3. **Round to the target currency's minor units.** JPY has 0, USD has 2, KWD has 3.
4. **Pick one rounding mode and use it everywhere.** Half-even unless a rule says otherwise.
5. **Do the arithmetic in integers or decimals, not binary floats.**
6. **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](https://allratestoday.com/blog/free-currency-data-api-iso-4217-metadata/) 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](https://allratestoday.com/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](https://allratestoday.com/blog/best-free-currency-exchange-api-2026/) and the [JavaScript converter guide](https://allratestoday.com/blog/free-currency-converter-api-javascript/).

## 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](https://allratestoday.com/register)

## Related Articles

- [Free currency converter API in JavaScript](https://allratestoday.com/blog/free-currency-converter-api-javascript/)
- [How to handle base currency conversion](https://allratestoday.com/blog/how-to-handle-base-currency-conversion/)
- [Exchange rates API for e-commerce](https://allratestoday.com/blog/exchange-rates-api-ecommerce/)
- [Free currency data API: ISO 4217 codes and metadata](https://allratestoday.com/blog/free-currency-data-api-iso-4217-metadata/)
- [How to get live exchange rates in Python](https://allratestoday.com/blog/how-to-get-live-exchange-rates-python/)
- [Free currency API comparison](https://allratestoday.com/blog/best-free-currency-exchange-api-2026/)
