Skip to main content
ZELSIS

Stripe charges 100x less: amount must be in cents

Your product costs $17.99, but the Stripe Checkout page shows $0.18, or the API rejects the request with an "Invalid integer" error. Stripe does not take prices in dollars. It takes an integer number of the smallest currency unit: cents for USD and EUR.

The cause

The amount field on PaymentIntents and the unit_amount field on Prices and Checkout line items are integers. 1799 means $17.99. Passing 17.99 fails because it is not an integer; passing 18 (a rounded dollar value) charges 18 cents, which is 100 times less than you meant.

Bad: price in dollars
const price = 17.99;

await stripe.paymentIntents.create({
  amount: price,          // rejected: not an integer
  currency: 'usd',
});

await stripe.checkout.sessions.create({
  mode: 'payment',
  line_items: [{
    quantity: 1,
    price_data: {
      currency: 'usd',
      unit_amount: Math.round(price), // 18 cents, not $17.99
      product_data: { name: 'Pro plan' },
    },
  }],
  success_url: `${origin}/success`,
});

The fix

Multiply by 100 and round. The rounding is not optional: floating point math gives 17.99 * 100 === 1798.9999999999998, which Stripe rejects, and truncating with Math.floor or | 0 would charge one cent too little.

Good: integer cents
function toCents(price: number): number {
  return Math.round(price * 100);
}

await stripe.paymentIntents.create({
  amount: toCents(17.99), // 1799
  currency: 'usd',
});

A better long-term fix is to store prices in cents everywhere (database column price_cents integer) and only divide by 100 when you display them. Then there is no conversion to forget. Never take the amount from the browser either: look the price up on the server by product id, or use a Stripe Price id.

Good: display from cents
const label = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
}).format(priceCents / 100); // "$17.99"

Zero-decimal currencies

"Multiply by 100" is only right for currencies with two decimal places. Some currencies have no minor unit, so the amount is the whole number: amount: 500 in jpy is 500 yen, not 5 yen. Examples include JPY, KRW, VND and CLP. A few currencies use three decimals (such as BHD, KWD and OMR). Stripe's currency documentation has the full list and some special cases, so check it before you add a new currency.

Convert per currency
const ZERO_DECIMAL = new Set(['jpy', 'krw', 'vnd', 'clp' /* see Stripe docs for the full list */]);

function toStripeAmount(price: number, currency: string): number {
  return ZERO_DECIMAL.has(currency.toLowerCase())
    ? Math.round(price)
    : Math.round(price * 100);
}

Verify it

Test it: In test mode, create a Checkout session or PaymentIntent for your most expensive and cheapest products and read the amount on the hosted page or in the Dashboard. Add one assertion to your tests, for example that toCents(17.99) returns 1799 and toCents(0.29) returns 29.

How Zelsis catches this

In files that use Stripe, Zelsis flags amount and unit_amount set to a decimal literal or to a variable holding one, and suggests the integer-cents fix.

  • BUG-10Stripe amount or unit_amount given in dollars instead of cents

More logic bugs from real code: bugs we found in popular open-source repos.

Check your own repo

Public repositories scan free. Paste a GitHub URL on the dashboard and every finding comes with the file, the line and a fix.

Scan a repository free