Your first discount should take an afternoon, not a week. Stripe supports coupons and promotion codes natively, but the docs split the concept across two objects -- and most Next.js tutorials stop at "create a coupon in the dashboard." Here is the full workflow so you can run a real promotion today.
Stripe separates discounts into two layers:
You create one coupon, then create one or more promotion codes that point to it. A customer types the code; Stripe validates it against the coupon rule. This matters because one coupon can have many codes -- a referral code for one user, a bulk discount for a partner, and a public launch discount all pointing to the same "20% first year" rule.
When a customer clicks "I have a promo code" during checkout, you have two options:
Option 1: Let Stripe handle it. Pass allow_promotion_codes: true to the Checkout Session. Stripe shows a built-in field and applies the discount automatically. This is the right default.
Option 2: Pre-apply a code. If you want to surprise the user -- auto-apply a referral code they got from a friend -- look up the promotion code ID from Stripe and pass it as discounts: [{ promotion_code: promoId }].
Here is what option 1 looks like in your checkout route:
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: stripeCustomerId,
line_items: [{ price: priceId, quantity: 1 }],
allow_promotion_codes: true,
success_url: `${baseUrl}/dashboard?checkout=success`,
cancel_url: `${baseUrl}/pricing`,
});
That one flag is all it takes for the most common case.
If a user arrives from a link like /signup?promo=LAUNCH20, you can apply the code automatically at checkout without them typing anything:
const promoCodes = await stripe.promotionCodes.list({
code: promoCodeParam,
active: true,
limit: 1,
});
const promoCode = promoCodes.data[0];
const session = await stripe.checkout.sessions.create({
// ...
discounts: promoCode ? [{ promotion_code: promoCode.id }] : [],
});
Store the code as a query param or in a short-lived cookie, then pull it out in the checkout route. This pattern works for launch specials, affiliate links, and partner codes.
Every time a promotion code is applied, Stripe fires a customer.discount.created webhook event. If you already handle Stripe webhooks for subscriptions (the Next.js SaaS Boilerplate ships this), you can add a new case:
case 'customer.discount.created': {
const discount = event.data.object;
const promoCodeId = discount.promotion_code as string;
// store against the customer in your DB
break;
}
This gives you a record of which users came in on a promotion -- useful for cohort analysis and support filtering.
Stackable discounts: by default, Stripe does not let two promotion codes apply at once. If you pre-apply a code and the user also types one at checkout, Stripe picks only one. Test this before your launch.
Expiry dates: set an expiry on the promotion code in Stripe, not just in your UI. A code that shows as expired on your landing page should also be inactive in Stripe, or a determined user can still apply it.
Subscription vs one-time: coupon duration options are "once", "repeating" (N months), and "forever". For a monthly SaaS, a "3-month 50% off" repeating coupon retains better than a one-time discount -- users feel the value for longer before they see full price.
Before you create your first promotion, answer three questions:
The fastest path: create a coupon in your Stripe dashboard with 30% off and an expiry date. Create a promotion code (BLACKFRIDAY25). Add allow_promotion_codes: true to your checkout route. Announce it. Stripe handles the validation, application, and webhook -- your app just needs to handle the success URL.
You do not need a feature flag system, a marketing platform, or a separate campaign tool to run a Black Friday deal. The infrastructure is already in your checkout flow.
If you want to run your first promotion without the weeks of setup, this Next.js SaaS boilerplate ships with Stripe subscriptions, webhooks, and checkout already wired up -- so a promo code is an afternoon's work, not a sprint.