Currency Conversion Rounding: How to Convert Money Without Floating-Point Errors

5 min read

Currency Conversion Rounding: How to Convert Money Without Floating-Point Errors

If you convert currencies with ordinary floating-point numbers, you will eventually get a total that is off by one cent. The fix is short: keep rates and amounts as exact decimals, multiply without rounding, and apply currency conversion rounding exactly once, at the moment you display or invoice the value.

This article explains why floats fail, which rounding rule to pick, how many decimals each currency needs, and how to put it together in JavaScript and Python. The same rules apply in any language.

Why floats break currency conversion

Binary floating point cannot represent most decimal fractions exactly. The classic demonstration works in JavaScript, Python and most other languages:

0.1 + 0.2;          // 0.30000000000000004
1.005 * 100;        // 100.49999999999999
(1.005).toFixed(2); // "1.00", not "1.01"

Each error is tiny, but money code does two things that magnify them: it multiplies by rates with four to six decimals, and it rounds the result. A value that should sit exactly on a rounding boundary lands just below it, and you lose a cent. Across thousands of invoices, your ledger and your customer's calculator disagree.

Floats also lose precision on large or very small values. Crypto amounts with 8 to 18 decimals, or a total in a currency with big nominal values, can exceed what a 64-bit float holds exactly.

The rule is simple: never let a float touch money. That includes the moment you parse JSON. If an API returns "rate": 1.0842 as a JSON number, your parser may already have converted it to a float. A rate returned as a string avoids that trap.

Keep decimals exact from API to database

Exactness has to survive every hop: API response, parser, calculation, storage.

  • Transport: receive rates and amounts as strings, not JSON numbers.
  • Calculation: use a decimal type (Decimal in Python, BigDecimal in Java, decimal in C#, bcmath or a decimal library in PHP) or integers scaled by a power of ten.
  • Storage: use NUMERIC or DECIMAL columns, never FLOAT or DOUBLE. Store the rate you used alongside the converted amount so the conversion can be audited later.

GetRatesAPI returns every rate and amount as a JSON string and does not round results. A response looks like this:

{"base":"EUR","quote":"USD","rate":"1.0842","as_of":"2026-01-02T10:00:00Z","status":"fresh","type":"direct"}

The rate value is exactly what the API computed. Rounding is your decision, because the right rounding depends on whether you are showing a price, charging a card or booking an invoice line.

Pick a rounding rule and a precision

Rounding mode

Two modes cover nearly everything:

ModeBehaviorTypical use
Half up2.5 becomes 3, 136.065 becomes 136.07Prices, invoices, most consumer-facing totals
Half even (banker's rounding)2.5 becomes 2, 3.5 becomes 4Accounting systems that sum many rounded values and want to avoid upward drift

Neither is universally correct. Check your accounting policy or local invoicing rules, pick one, and apply it everywhere. Many language defaults surprise people: Python's built-in round() uses half even, while JavaScript's Math.round rounds halves toward positive infinity. State the mode explicitly in code instead of relying on defaults.

Decimal places per currency

Not every currency has two decimals. The ISO 4217 standard defines minor units per currency:

CurrencyMinor unit digitsExample
USD, EUR, GBP2136.07
JPY, KRW020,415
KWD, BHD341.352

Hard-coding two decimals will overcharge or undercharge in zero- and three-decimal currencies. Keep a small lookup table keyed by currency code, with a default of 2. For crypto, choose a display precision that suits the asset and keep full precision internally.

Round once, at the end

The most common source of drift is rounding at every step. Follow these habits:

  1. Multiply first, round last. Compute amount × rate with full precision, then round the result a single time.
  2. Do not chain conversions. Converting USD to EUR and then EUR to GBP, rounding in between, gives a different answer than USD to GBP directly. Ask for the pair you need.
  3. Do not invert rates yourself. Computing 1 / rate produces a repeating decimal that you must truncate somewhere. Request the reverse pair instead. GetRatesAPI works for any pair, deriving it through USD, EUR or BTC when no market quotes it directly, and each response says whether it was direct or derived.
  4. Decide line-item versus total rounding up front. Rounding each line then summing can differ from summing then rounding. Either is acceptable if it is a documented policy, and the customer-facing invoice must add up.

Example: Python with Decimal

Python's decimal module does this cleanly. This script calls the convert endpoint, reads the exact string, and quantizes it to two places:

import os
from decimal import Decimal, ROUND_HALF_UP

import requests

resp = requests.get(
    "https://api.getratesapi.com/v1/convert",
    params={"from": "USD", "to": "EUR", "amount": "125.50"},
    headers={"Authorization": f"ApiKey {os.environ['GETRATESAPI_KEY']}"},
    timeout=10,
)
resp.raise_for_status()
data = resp.json()

# Parse the string directly. Never pass it through float().
exact = Decimal(data["converted_amount"])
display = exact.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(exact, "->", display)

If you already hold a rate and want to calculate locally, Decimal("125.50") * Decimal("1.0842") gives exactly 136.067100, which rounds half up to 136.07. Note the amount is passed as a string. Decimal(125.50) starts from a float and defeats the purpose, and with a value like Decimal(1.1) you would inherit the binary error.

Example: JavaScript without floats

JavaScript has no built-in decimal type, but BigInt works for exact arithmetic. This sketch handles positive values only and scales both numbers to integers:

function parseDecimal(str) {
  const [whole, frac = ""] = str.split(".");
  return { int: BigInt(whole + frac), scale: frac.length };
}

function multiplyAndRound(amountStr, rateStr, places) {
  const a = parseDecimal(amountStr);
  const r = parseDecimal(rateStr);
  const product = a.int * r.int;
  const scale = a.scale + r.scale;

  let rounded;
  if (scale <= places) {
    rounded = product * 10n ** BigInt(places - scale);
  } else {
    const divisor = 10n ** BigInt(scale - places);
    const quotient = product / divisor;
    const remainder = product % divisor;
    rounded = remainder * 2n >= divisor ? quotient + 1n : quotient; // half up
  }

  const digits = rounded.toString().padStart(places + 1, "0");
  return places === 0
    ? digits
    : `${digits.slice(0, -places)}.${digits.slice(-places)}`;
}

console.log(multiplyAndRound("125.50", "1.0842", 2)); // "136.07"

For production code with negative numbers and multiple rounding modes, use a well-tested decimal library rather than extending this sketch. The principle stays the same: the input strings never become floats.

Where GetRatesAPI helps

Because the API returns strings and leaves results unrounded, the pipeline above has no hidden precision loss. The convert endpoint returns converted_amount for an exact conversion, and the current rate endpoint returns the rate if you prefer to calculate locally. Both cost one request unit.

A few practical notes:

  • Store the rate, as_of and type fields next to every converted amount you persist. They explain the number later.
  • Check status before charging. A stale rate is flagged, not withheld, so your code decides what to do. For caching strategies that keep this safe, see how to cache exchange rates without showing stale prices.
  • Validate input amounts before sending them. A malformed amount returns an invalid_amount error (HTTP 400), and malformed requests are not billed. Match on the code field, not the message text.

Checklist before you ship

  • No floats anywhere between the API response and the database.
  • Rates and amounts parsed from strings.
  • A documented rounding mode, set explicitly in code.
  • Minor-unit digits looked up per currency.
  • One rounding step, at the end.
  • Rate, timestamp and status saved with each conversion.

Try it with exact decimals

You can test the conversion examples above on the free plan with no credit card: create an account, copy a key, and use the dashboard playground to call the convert endpoint. If you need more volume later, see the pricing page for plan limits.

All articles →
How to Cache Exchange Rates Without Showing Stale Prices

6 min read

How to Cache Exchange Rates Without Showing Stale Prices

Calling a rates API on every page view wastes quota and slows pages. Here is how to cache exchange rates with the right TTL for each use case, and how to fall back safely when the API is unavailable.

Currency Conversion Rounding Without Float Errors — GetRatesAPI