Stripe amounts are a hundred times off in JPY and other zero-decimal currencies
A customer in Tokyo was charged 499,900 yen for a plan that costs 4,999 yen. Or the mirror image, where a Japanese price shows up as a hundredth of what it should be and nobody notices for a year because that currency is a rounding error on the revenue chart.
Both come from the same line of code.
The rule Stripe actually applies
Every amount in the Stripe API is an integer in the currency’s minor unit.
Currencies are two-decimal unless Stripe says otherwise. For USD the minor unit
is the cent, so 20 dollars is 2000. This is where the reflex to multiply by
100 comes from, and it is correct for the currencies most teams test in.
Japanese yen has no smaller unit. 2,000 yen is 2000, not 200000. Korean won
behaves the same way, and so does the rest of Stripe’s zero-decimal list.
Four currencies are special-cased on top of that. ISK and UGX moved to zero
decimals but still have to arrive as two-decimal values whose last two digits
are 00, so 5 ISK is 500 and a fraction of one is refused. HUF and TWD can be
charged as two-decimal amounts but are treated as zero-decimal for payouts, so a
manual payout in either has to be evenly divisible by 100.
The authoritative list lives in Stripe’s currency documentation. It does not
live in the constant 100 sitting in your money helper.
Why it survives so long
The wrong number is still a plausible number. Nothing about 499900 looks like
corruption, and it renders as a clean figure on the invoice.
Nobody on the team holds a card in that currency, so no internal purchase ever walks the path. The person who does walk it is the customer you just charged a hundred times too much, and their response is a dispute rather than a bug report.
The undercharge direction is worse. A customer paying a hundredth of the price has every reason to say nothing at all, and the error compounds quietly for as long as you sell in that currency.
Confirming it
Pull your Price objects for every currency other than your home one and read
unit_amount, which Stripe defines as the amount in the currency’s minor unit.
A JPY price whose amount reads like a dollar figure with two zeros bolted on is
the tell. An ISK price that is not a multiple of 100 is the same tell running
the other way.
Then take one finalized invoice per foreign currency and compare its total against what your own pricing page promised for that plan. The bug is per currency rather than per customer, so a single invoice settles the question for everybody billed in that currency.
Fixing it
Put one conversion between your internal money type and Stripe amounts, driven by a per-currency exponent table rather than by a constant. Stripe publishes which currencies are zero-decimal and which are special-cased, and the table belongs in code where a test can read it.
If you are setting foreign prices by hand, currency_options on a Price holds a
unit_amount per currency, so Stripe stores the number instead of your code
computing it at call time.
Then repair the money. Overcharges of this magnitude need refunds or credit notes, and quickly, because a hundred-fold charge becomes a chargeback and a chargeback costs more to handle than the charge was worth. Undercharges need a decision about how far back you are willing to go.
Currency precision never throws. The wrong number is a valid integer in a valid currency, and it clears.