Guide · facts checked 4 Nov 2026
Apply a retention discount to a Stripe subscription, step by step
Create a coupon with a duration, then apply it to the subscription with a subscription update that sets discounts[0][coupon]. Two details decide whether it goes well: a populated discounts array overwrites any discount already on the subscription, and a coupon cannot be limited to one customer, so the guard against giving the same person a second offer has to live in your code.
The two calls
- Create the coupon once. A percentage off, with a duration of
once,repeating(plusduration_in_months) orforever. A retention offer is normallyrepeatingfor a few months. - Apply it when the customer accepts. Update the subscription, set
discounts[0][coupon]to the coupon ID and setcancel_at_period_end=falsein the same update.
curl https://api.stripe.com/v1/coupons \
-u "$STRIPE_SECRET_KEY:" \
-d id=retention-20-3mo \
-d percent_off=20 \
-d duration=repeating \
-d duration_in_months=3
curl https://api.stripe.com/v1/subscriptions/sub_123 \
-u "$STRIPE_SECRET_KEY:" \
-d "discounts[0][coupon]=retention-20-3mo" \
-d cancel_at_period_end=false
The parameters and the discounts[0][coupon] pattern come from Stripe’s coupons page and its create-coupon and update-subscription references. Run both calls in Stripe test mode first, with a sk_test_ key, before they touch a live subscription.
If the cancel is already scheduled, set cancel_at_period_end=false in the same update; the coupon alone does not undo it. That same flip is what the webhook in the cancel-flow guide treats as a candidate save.
Pick the coupon duration
| duration | What Stripe applies | Use it for |
|---|---|---|
once | The first invoice only (the default). | A single month off for a monthly plan. |
repeating + duration_in_months | Every invoice in that many months from first application. | A common shape: 20–50% for 2–4 months. |
forever | Every invoice, indefinitely. | Rarely a good cancel offer: the discount becomes the price. |
Stripe’s own example: a 50% coupon with duration_in_months=4 covers the first 4 invoices on a monthly subscription. On an annual subscription the same coupon discounts the whole year if it renews within those months. Match the duration to the plan interval before you promise a customer anything. Which offer fits which reason is in pause vs discount vs downgrade.
Trap 1: a populated discounts array overwrites
Stripe’s update reference says: a populated discounts array overwrites the existing discounts on the subscription; an empty array leaves them unchanged; an empty string clears them. So if the customer already has a discount, sending only the retention coupon replaces it. Read the subscription’s current discounts before you update, and include the ones you want to keep in the array. For the exact shape of each entry, open the discounts parameter of the update subscription reference. Stripe allows up to 20 discounts on a subscription, shared between coupons and promotion codes.
Trap 2: coupons cannot be limited to one customer
Stripe’s comparison table marks “restrict to a specific customer” and “first purchase only” as supported for promotion codes and not for coupons. A coupon applied from your back end is the right tool for a retention offer, because the customer never types a code and cannot pass it on. But it also means nothing in Stripe stops you applying the same coupon to the same customer next month. Store the offer you made, with the date, and check it before you show the offer again.
max_redemptions caps total uses across all customers, and each application counts, including several to the same customer. Use it as a ceiling on spend, not as a per-customer rule.
Know what a once coupon looks like afterwards
With duration=once, the coupon counts as used once the invoice is finalised, and Stripe removes it from the subscription’s discounts array; the invoice still shows the discount. Stripe notes that a subscription can therefore look as if it has no discount even though one was applied. Read the invoice, not just the subscription, when you count how many customers took the offer.
Before you ship
- Apply the discount only on the customer’s explicit acceptance, and keep “Cancel anyway” one click away.
- Cap the depth and length of the offer in code, not in the coupon alone.
- Record the offer and the date per customer before the API call, so a retry cannot apply it twice.
- Judge whether the offer saved anyone against a holdout, not against everyone who accepted: what counts as a save.
The surrounding flow, including the webhook that tells you a customer scheduled a cancel, is in how to build a cancel flow with Stripe.
Let the offer follow the reason
Run a sample cancel session: play a subscriber, give a reason, and see which offer is chosen and the Stripe change behind it.
90 seconds, no sign-up. You play the subscriber; the AI answers, and “Cancel anyway” is always one click away.