Ask a payment API to charge 1999 and it will charge ₹19.99, not ₹1,999. That convention, amounts as whole numbers of the smallest currency unit, catches out almost every developer once. It exists for a good reason: the obvious way to store money in most programming languages, a floating-point number, can't represent most decimal amounts exactly, and payment systems can't tolerate "almost".
This guide covers how to represent money correctly from the API to the database: the problem with floats, integer minor units, how many decimal places each currency really has, rounding and splitting amounts so nothing is lost, converting currencies, and formatting amounts for display.
Why floating point and money don't mix
Floating-point numbers store values in binary. Just as one third can't be written exactly in decimal, one tenth can't be written exactly in binary. The error is tiny, but it's there, and it surfaces in comparisons, totals and rounding:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
1.15 * 100 // 114.99999999999999 (so Math.floor gives 114, not 115)The last line is the classic payments bug: converting ₹1.15 to paise by multiplying by 100 and truncating produces 114 paise. Add enough of these and reconciliation stops matching. The same happens with floats in Python, Java, Go and most other languages, and with FLOAT or DOUBLE columns in databases. PostgreSQL's own documentation says floating-point numbers shouldn't be used to handle money because of rounding errors.
The options, compared
There are three sound ways to hold money, and one to avoid:
| Representation | Example for ₹19.99 | Verdict |
|---|---|---|
| Integer minor units | 1999 (paise), stored as a 64-bit integer | The standard for payment APIs and ledgers |
| Exact decimal type | 19.99 in a DECIMAL or NUMERIC column, or a decimal library | Good, especially where you need fractions of a minor unit, such as rates |
| String in an API | "19.99" parsed into a decimal type | Fine at boundaries if parsed strictly; never compute with strings |
| Floating point | 19.99 as a float or double | Avoid for money |
Most payment APIs take amounts as integers in the smallest unit, paise for rupees; LILPE's unified API page describes amounts the same way. Inside your own system, integers in minor units are the simplest choice for stored amounts, with a decimal type for anything that needs more precision, like exchange rates or percentage fees before rounding.
How many decimal places does a currency have?
Not every currency has two. The ISO 4217 standard, maintained by SIX on behalf of ISO, publishes each currency's "minor unit": the number of decimal places used. Don't hard-code 100:
| Currency | Code | Minor unit | An amount of 1999 means |
|---|---|---|---|
| Indian rupee | INR | 2 | 19.99 |
| US dollar | USD | 2 | 19.99 |
| Euro | EUR | 2 | 19.99 |
| Japanese yen | JPY | 0 | 1,999 |
| Kuwaiti dinar | KWD | 3 | 1.999 |
| Bahraini dinar | BHD | 3 | 1.999 |
Keep a currency table from the ISO list in your code, keyed by the three-letter code. Some payment providers use their own decimal conventions for a few currencies, so check each provider's documentation where it differs, and convert at the connector rather than throughout your system.
Always store the currency next to the amount. An amount without a currency is a bug waiting for your first international customer.
Integer size
Use a 64-bit integer (BIGINT in SQL) for minor units. A 32-bit integer tops out at a little over 2.1 billion, which is about ₹2.1 crore in paise: too small for a large payment, let alone a daily total.
JavaScript needs care. Its standard Number type represents integers exactly only up to 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER). That's ample for individual payments in paise, but sums over large volumes, or currencies with three decimals and big amounts, can get close. Use BigInt for aggregates, or keep arithmetic in the database, and consider sending large values as strings in JSON.
Rounding: decide the rule, then apply it once
Fees, taxes, commissions, discounts, interest and currency conversion all produce fractions of a paisa. You have to round, and two decisions matter:
- The rounding mode. Half-up (0.5 rounds away from zero) is what most people expect; half-even, or banker's rounding (0.5 rounds to the nearest even digit), avoids a small upward bias over many operations. Your finance and tax teams should choose, because invoices, tax and statements must agree.
- Where you round. Rounding each line and then summing can give a different total from summing and then rounding once. Decide which your documents use, and do it the same way everywhere.
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN
fee_paise = Decimal("1999") * Decimal("0.0175") # Decimal('34.9825')
fee_paise.quantize(Decimal("1"), rounding=ROUND_HALF_UP) # Decimal('35')
Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP) # Decimal('3')
Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_EVEN) # Decimal('2')Note the string arguments: Decimal("0.0175") is exact, while Decimal(0.0175) would faithfully copy the float's binary error into your decimal.
Splitting amounts without losing a paisa
Splitting ₹10.00 three ways can't give three equal parts in paise: 333 + 333 + 333 is 999. Somebody gets the extra paisa, and your code has to decide who, or the parts won't add back to the whole. The same applies to splitting a payment between sellers, spreading a discount across items, or dividing an amount into instalments.
function allocate(totalMinor: bigint, weights: number[]): bigint[] {
const totalWeight = BigInt(weights.reduce((a, b) => a + b, 0));
// Each part rounded down; BigInt division truncates.
const parts = weights.map((w) => (totalMinor * BigInt(w)) / totalWeight);
let remainder = totalMinor - parts.reduce((a, b) => a + b, 0n);
// Hand out the leftover minor units one at a time, starting with the first part.
for (let i = 0; remainder > 0n; i = (i + 1) % parts.length) {
parts[i] += 1n;
remainder -= 1n;
}
return parts;
}
allocate(1000n, [1, 1, 1]); // [334n, 333n, 333n] (₹10.00 split three ways)
allocate(1999n, [70, 30]); // [1400n, 599n] (70/30 split of ₹19.99)This version handles positive totals; allocate refunds by their absolute value and apply the sign afterwards. Whatever method you use, the test is simple: the parts must always add up to the total.
Converting between currencies
Currency conversion is where precision rules matter most, because rates have many decimal places:
- Store exchange rates in a decimal type with enough precision, never as floats, along with their source and timestamp.
- Convert from minor units to minor units: amount in the source currency's minor units, times the rate, adjusted for the difference in minor-unit exponents, rounded once with your chosen mode.
- Record the rate used on the transaction, so any amount can be recomputed and explained later.
- Treat a conversion as a transaction between accounts in two currencies, not as a change to an existing balance. Our guide to double-entry ledgers shows the pattern.
Our guide to multi-currency payments explains where conversion happens in a payment and who sets the rate.
Formatting for display
Formatting belongs at the edge, in the interface, never in stored data. Let the platform do it rather than concatenating symbols and commas:
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR" });
inr.format(1999 / 100); // "₹19.99"
inr.format(100000); // "₹1,00,000.00" (Indian lakh grouping)Dividing by 100 here is acceptable because it's for display only and the result is never stored or used in arithmetic; for currencies with other minor units, divide by the right power of ten from your currency table. Intl applies each locale's grouping, which is why Indian customers see lakh and crore grouping with the en-IN locale.
Designing amounts in an API
If you design a payments API, or an internal service that handles money, settle these once and document them:
- Amounts are integers in the currency's minor units, with an ISO 4217 currency code on every amount.
- Reject decimal points in amount fields rather than guessing what was meant.
- Validate ranges: no zero or negative amounts where they make no sense, and a sensible maximum per currency.
- Return amounts the same way you accept them, including in webhooks and reports.
- State the rounding rule for anything your API calculates, such as fees or taxes.
A checklist
Before shipping anything that handles money:
- No floats or doubles anywhere money is stored, sent or calculated.
- Amounts in minor units as 64-bit integers, or in an exact decimal type, always with a currency code.
- Minor units per currency from the ISO 4217 list, not a hard-coded 100.
- A documented rounding mode, applied at a documented point.
- Splits that always sum to the original total, with tests.
- Exchange rates stored as decimals with source and time, and recorded on each conversion.
- Formatting only in the interface, using locale-aware formatting.
Frequently asked questions
What is the best way to store money in a database?
As an integer number of minor units (paise, cents) in a 64-bit integer column, with the currency code in another column, or in an exact DECIMAL or NUMERIC column. Never use FLOAT, DOUBLE or REAL for money.
Why do payment APIs use amounts in paise or cents?
Because whole numbers are exact. Representing ₹19.99 as 1999 paise avoids the rounding errors of floating-point numbers and makes it unambiguous how much is being charged.
Why shouldn't I use float for money?
Floating-point numbers are stored in binary and can't represent most decimal amounts exactly, so values like 0.1 carry tiny errors that show up in comparisons, totals and conversions, such as 1.15 times 100 coming out as 114.99999999999999.
Do all currencies have two decimal places?
No. ISO 4217 lists the minor unit for each currency: INR, USD and EUR use two, the Japanese yen uses none, and the Kuwaiti and Bahraini dinars use three. Look the value up rather than assuming 100.
How should I round amounts in payments?
Choose a rounding mode with your finance team, such as half-up or half-even, decide whether you round per line or on the total, and apply that rule consistently. When splitting an amount, distribute the remainder so the parts always add up to the original.
Official sources
- SIX — ISO 4217 currency codes (list one, published 17 September 2026)Minor units: INR, USD and EUR 2; JPY 0; KWD and BHD 3.
- PostgreSQL documentation — Monetary typesFloating-point numbers should not be used to handle money due to the potential for rounding errors.
- PostgreSQL documentation — Numeric typesNUMERIC stores exact values; recommended where exactness is required, such as monetary amounts.
- MDN — Number.MAX_SAFE_INTEGER
- Python documentation — decimal: decimal fixed-point and floating-point arithmetic
Last reviewed . Examples, amounts and screens marked illustrative are not LILPE figures.




