Stripe coupon attached to the subscription, no discount on the invoice
A customer forwards you an invoice for the full amount. You open the subscription in Stripe and the coupon is sitting right there, active, nowhere near its expiry. The discount is attached to the subscription and the invoice charged the list price anyway.
The usual cause is the coupon’s product restriction.
What Stripe is doing
A coupon can be created with applies_to[products], an array of Product IDs
the coupon is allowed to reduce. Stripe applies it only to invoice lines whose
price belongs to one of those products. Add that coupon to a subscription with
no applicable product and no discount is applied. The coupon stays attached and
valid. The amount it takes off is zero.
Promotion codes inherit the product restrictions of the coupon behind them, so a code your customers type in fails the same way.
From the API side none of this looks like a failure. The subscription update returned 200. The discount is present on the object when you fetch it. Invoice finalization succeeded. A test that asserts the coupon is attached passes, because it is attached. Attached and applied are two different facts, and only one of them moves the total.
The reverse is worth knowing too. A coupon with duration=once is consumed when
the invoice finalizes and drops out of the subscription’s discounts array, so
a subscription that did get its discount can look like it never had one.
The restriction usually stops matching for a reason nobody wrote down. A price gets rebuilt under a new product during a repackaging. A plan is cloned instead of edited and the clone carries a fresh product ID. The coupon was written against last year’s product catalog and has not been re-read since.
Confirming it
Read total_discount_amounts on the invoice. Each entry pairs a discount with
the amount it actually removed. A zero next to the coupon you were counting on
is the whole answer.
Then compare the two lists by hand. Fetch the coupon and read
applies_to.products. Fetch the invoice lines and read
pricing.price_details.product on each one. An empty intersection means the
coupon was never going to do anything on that invoice.
Do it for the customer who complained. Then do it for every subscription carrying the same coupon. A product restriction that stopped matching stopped matching for all of them on the same day, and only one customer bothered to write in.
Fixing it
applies_to is set when the coupon is created and cannot be edited afterwards.
The update endpoint takes name, metadata and currency_options, and nothing
else. So the repair is a new coupon with a correct product list, or with no
product restriction at all if the discount was always meant to cover the whole
account. Attach the new discount to the affected subscriptions and remove the
stale one. Deleting the old coupon stops new applications without removing the
discount from anything that already carries it.
Then deal with the invoices that already went out, because the customer has been paying more than they agreed to on every cycle since the restriction went stale. A credit note against the finalized invoice puts the correction on the original document, which is what you want if tax or revenue reporting has to reflect it. Stripe accepts one against an invoice that is open, paid or uncollectible. A customer balance credit is simpler and lands on the next invoice instead.
Work out the number before deciding it is small. A ten percent discount that quietly stopped applying to a $900 monthly subscription eleven months ago is $990 per customer, and the customer has no copy of what the invoice was supposed to say.
This one belongs to the family of billing failures that return a clean 200 and a wrong number, and a few more of them are collected here.