Adding seats in Stripe produced a refund instead of a charge
The customer bought more seats. The invoice for that change came out negative and money moved toward them.
This is the strangest billing failure we have hit first-hand, and it survived months of nobody noticing, because every invoice in the sequence added up on its own terms.
The combination that causes it
Two ordinary things were true at once. The subscription had been created with a
start date in the past, which is what backdate_start_date is for and what
every migration off an old billing system reaches for. And the subscription
carried a coupon.
When you raise the quantity on a subscription item mid-period, Stripe writes two proration lines. One is a credit for unused time on the old quantity. The other is a charge for the remaining time at the new one. Both are marked as prorations and Stripe prorates down to the second. On an increase the charge should be the larger of the two.
What the credit is measured against depends on the subscription’s billing mode.
Under billing_mode=classic, Stripe calculates a credit proration from the
subscription item’s current price, tax, quantity and the last discounts used,
not from the amount that was originally billed. Under billing_mode=flexible it
uses the original debit. Stripe’s own worked example of the classic calculation
credits time at a price the customer never paid, and the invoice lands at a
negative total.
Discounts touch prorations in one direction only. Stripe computes proration
credits and debits from the subscription’s discounted price, then marks the
proration lines discountable false so no further discount lands on them. A
discount on an invoice that mixes prorations with ordinary lines reduces only
the ordinary ones.
In our case the initial payment had been computed without the discount and the proration lines were computed with it. The credit and the charge were therefore measured against two different prices. They did not cancel the way the arithmetic assumes, and on a seat increase the delta landed below zero. Buying more produced a refund.
Why nobody catches this
Every invoice involved is internally consistent. Recompute any one of them from its own lines and it agrees with itself.
The party receiving money is the customer, so no support ticket is ever filed. And nobody writes a test asserting that buying more costs more, because it is not the kind of thing that can go wrong.
Months later somebody reconciles a spreadsheet and finds expansion revenue flowing backwards on exactly the accounts that were growing.
Confirming it
Two places to look.
Start with finished invoices. Any invoice whose total is at or below zero, or
any customer whose credit balance grew on a day their seat count went up, is
worth opening. The proration lines are the ones where
parent.subscription_item_details.proration is true. Check whether the credit
exceeds the charge.
Then look at the population that can produce it. Subscriptions created with a back-dated start that also carry a discount are the ones to walk. Compare what each expansion actually billed against what the added seats cost for the time remaining in that period.
Fixing it
For the accounts already affected, work out what the expansion should have cost and bill the difference as a one-off invoice item. Be ready to explain it, since the customer has already seen the credit.
On the change path, preview before you commit. POST /v1/invoices/create_preview
takes the subscription changes you are about to make and returns the invoice
they would generate, without touching the subscription. Pass
subscription_details.proration_date on the preview and the same
proration_date on the update, or the two calculations drift, because Stripe
prorates to the second. A quantity increase that previews as a negative total is
something your code can refuse to apply, on the spot, with an alert instead of a
refund.
For migrations, keep backdating and discounts apart. A coupon on a backdated
subscription starts its duration at the backdated start date rather than at the
API call, so a repeating coupon whose duration is shorter than the backdated
period is spent before the first live invoice and is removed from the
subscription item. Bill the historical period as its own invoice and let the new
subscription start now, or attach the coupon after the subscription has reached a
normal cycle. The pair is what makes the arithmetic disagree.
Wrong prorations are one instance of a broader class, where Stripe executed correctly against a configuration nobody intended, and other members of that class are here.