Can you stack multiple coupons in Stripe Checkout?

No. A Stripe Checkout Session supports up to one coupon or promotion code, whether your server passes it or the buyer types it. Stripe stacks up to 20 discounts on subscriptions and invoices, but not in Checkout. To honor two codes in one checkout, you merge them into a single coupon on your server, which makes you the discount engine.
Verified against official documentationReviewed Send a correction (opens in a new tab)

The short answer on Flint

Flint's hosted checkout lets the buyer enter a code and then add another code, and Flint's promotions engine decides what stacks. Automatic offers, such as free shipping over $75, apply on their own. Every candidate is ranked by the discount it gives, and your rules decide which survive: which discount classes a promotion combines with, exclusive groups, a stop-after flag, and a per-order maximum. A code that loses comes back with a reason you can show the buyer. Card processing runs on Stripe underneath. The Flint requests are below.

Stripe coupon vs promotion code

The two are layers of one discount. A coupon is the discount: a percentage or amount off, how long it lasts on a subscription, which products it covers with applies_to, and how many times it can be redeemed. Your server applies a coupon, and the buyer never types it. A promotion code is a string the buyer types, such as SPRING10, that points at one coupon and adds conditions on top.

Stripe couponStripe promotion code
Who applies itYour server, in discountsThe buyer, in the code field
Restrict to one customerNoYes
First-time customers onlyNoYes
Minimum order amountNoYes
Limit to some productsapplies_to.productsInherited from its coupon
Per Checkout SessionOne coupon or one promotion code, not both

Many promotion codes can point at the same coupon, so FALLPROMO and SPRINGPROMO can share one 25% off coupon. None of that changes the limit: a session takes one of them.

The workaround: merge the codes into one coupon

The only way to honor two codes in one Checkout Session is to collect them on your own cart page, decide what they are worth together, and create a one-time coupon for that amount:

JavaScript
// Your cart page collected several codes. Stripe will take one discount.
const codes = [];
for (const typed of req.body.codes) {
  const { data } = await stripe.promotionCodes.list({ code: typed, limit: 1 });
  const code = data[0];
  if (!code || !code.active) return res.status(400).json({ error: `${typed} isn't valid` });
  codes.push(code);
}

// Your discount engine: each code's restrictions (customer, first order,
// minimum amount, expiry, uses so far), which codes may combine, in what order
// percent and fixed amounts apply, and which products each one covers.
const amountOff = await priceStackedDiscount(cart, codes);

const coupon = await stripe.coupons.create({
  name: "Combined discount", // Stripe caps coupon names at 40 characters
  metadata: { promotion_codes: codes.map((c) => c.code).join(",") },
  amount_off: amountOff,
  currency: "usd",
  duration: "once",
  max_redemptions: 1,
});

const session = await stripe.checkout.sessions.create({
  mode: "payment",
  line_items: cart.lineItems,
  discounts: [{ coupon: coupon.id }], // the session's one discount
  metadata: { promotion_codes: codes.map((c) => c.code).join(",") },
  success_url: "https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}",
});

The receipt then shows one discount named SPRING10 + VIP20, and priceStackedDiscount is a discount engine you now own. It has to enforce every restriction Stripe would have checked, decide which offers may combine, apply them in a fixed order, and count each code's uses, because Stripe only sees the merged coupon. Each of those is a place where buyers get a discount you didn't intend:

Where merged discounts go wrong

  1. The codes the buyer typed are never redeemed#

    SPRING10 was capped at 500 uses and has been used 900 times.

    When you merge codes into a one-off coupon, Stripe redeems that coupon, not the promotion codes the buyer entered. Their max_redemptions and expiry stop limiting anything, because Stripe never sees them at checkout.

    Confirm it

    Compare the promotion code's redemption count in the Dashboard with the number of completed sessions whose metadata lists that code.

    Fix

    Count uses of each code in your own database when checkout.session.completed arrives, and refuse the code in your cart when your count reaches its limit. Release the count when a session expires unpaid.

  2. Every restriction becomes your code#

    A first-order code went to a returning customer, and a $50-minimum code applied to a $30 cart.

    Stripe checks a promotion code's restrictions (a specific customer, first-time transaction, minimum amount, expiry) when the code is redeemed. A merged coupon carries none of them, so each check you skip is a discount Stripe would have refused.

    Confirm it

    List the restrictions on every active promotion code and find the line in your cart code that enforces each one.

    Fix

    Read each code's restrictions when the buyer enters it and enforce them on the subtotal your server computes. Recheck them when the cart changes, since a removed item can drop the cart under a minimum.

  3. Percent then dollars is a different total from dollars then percent#

    Two buyers with the same cart and the same two codes paid different amounts.

    Stripe's own stacking docs for subscriptions note that 20% off then $5 off gives a different result from $5 off then 20% off. When you merge codes yourself, the order is your rule, and if it follows the order the buyer typed the codes in, the total changes with it.

    Confirm it

    Apply the same two codes in both orders to the same cart in your cart code and compare totals.

    Fix

    Fix one order in code, such as percentage discounts before fixed amounts, and write it down where support can read it.

  4. An automatic discount fills the only slot#

    You pass a spend-over-$50 coupon from your server, and now buyers can't use their newsletter code.

    A Checkout Session holds up to one coupon or promotion code. When your server passes a coupon in discounts to make an offer automatic, that is the session's one discount, and a code the buyer has has nowhere to go.

    Confirm it

    Search your session code for every path that sets discounts, and list which of those pages also tell buyers they can enter a code.

    Fix

    Decide the winner before the session exists: compare the automatic offer with any code on your own cart page, or merge them into one coupon with the costs above.

  5. A second discount can't be a negative line item#

    You tried a $-5.00 "Loyalty credit" line and the request failed.

    A Price's unit_amount is a positive integer, or 0 for a free price. Checkout line items come from Prices, so a discount can't ride along as a negative line.

    Confirm it

    Search your integration for discount logic that builds price_data with an amount below zero.

    Fix

    Fold the extra discount into the one coupon, or lower the price of the item itself with price_data and record why in metadata.

On Flint, stacking is a set of rules

A Flint promotion is a discount rule with a redemption type: code when the buyer enters a code, or automatic when Flint applies it to every order that qualifies. One promotion can have many codes, each with its own use limit and expiry. This VIP code is worth 20% and refuses to share an order with any other order-wide discount:

cURL
curl -X POST https://api.withflintpay.com/v1/promotions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: promotion-vip20-001" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "VIP 20",
    "display_name": "20% off for VIPs",
    "redemption_type": "code",
    "discount_class": "order",
    "application_method": { "type": "percent_off", "percent_off": 20 },
    "combines_with": { "order": false, "line_item": true, "service_charge": true },
    "codes": [{ "code": "VIP20", "max_uses": 200 }]
  }'

An automatic promotion needs no code. This one waives shipping on orders over $75 and applies alongside a code, because it discounts a different class, the shipping charge:

JSON
{
  "name": "Free shipping over 75",
  "display_name": "Free shipping on orders over $75",
  "redemption_type": "automatic",
  "discount_class": "service_charge",
  "eligibility_rules": [
    { "attribute": "order.subtotal", "operator": "gte", "values": [{ "amount": 7500, "currency": "USD" }] }
  ],
  "application_method": {
    "type": "percent_off",
    "percent_off": 100,
    "discounted_item_rules": [
      { "attribute": "charge.type", "operator": "eq", "values": ["shipping_fee"] }
    ]
  }
}

When a buyer brings two codes, send both. Flint ranks the candidates by the discount each would give, admits them largest first, and skips any that the rules exclude:

cURL
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: order-two-codes-001" \
  -H "Content-Type: application/json" \
  -d '{
    "line_items": [{
      "name": "Rain jacket",
      "quantity": 1,
      "unit_price_money": { "amount": 12000, "currency": "USD" },
      "fulfillment": { "requirement": "none" }
    }],
    "discounts": [
      { "promotion": { "promotion_code": "spring10" } },
      { "promotion": { "promotion_code": "vip20" } }
    ]
  }'

VIP20 is worth $24.00 on this $120.00 order and SPRING10 is worth $10.00, so VIP20 is admitted first. Its combines_with refuses other order-wide discounts, so SPRING10 is skipped, and the request still succeeds with a warning (abridged):

JSON
{
  "data": {
    "applied_discounts": [{
      "customer_facing_name": "20% off for VIPs",
      "promotion_code": "VIP20",
      "applied_money": { "amount": 2400, "currency": "USD" },
      "status": "pending"
    }],
    "pricing_amounts": {
      "subtotal_money": { "amount": 12000, "currency": "USD" },
      "discount_money": { "amount": 2400, "currency": "USD" },
      "total_money": { "amount": 9600, "currency": "USD" }
    }
  },
  "meta": {
    "warnings": [{
      "code": "promotion_declined",
      "severity": "warning",
      "reason": "not_combinable",
      "resource": "promotion",
      "promotion_code": "SPRING10"
    }]
  }
}

Branch on reason, not on message text. The others you will see most are max_promotions_reached, superseded_by_better_offer for an exclusive group, and minimum_not_met, which comes with the amount the cart still needs in gap_money. Three fields shape stacking:

  • combines_with lists the discount classes (order, line item, and charges such as shipping) a promotion can share an order with. Both promotions must allow each other.
  • exclusivity puts promotions in a group where only one applies: the best one, or the one with the highest priority.
  • stacking_mode: "stop_after" admits the promotion and skips every lower-ranked one.

Your account sets how many promotions one order can carry, 2 by default and up to 10. Manual discounts you add from your own code always stay and don't count toward it:

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "promotions": { "max_promotions_per_order": 3 } }'

On hosted checkout the buyer types a code, sees it as a removable chip, and can add another. Automatic promotions show as named rows in the totals, and a valid code that didn't apply is explained in a note rather than shown as an error. In your own cart, preview a code before applying it, including what it would knock off and which offers it would replace:

cURL
curl -X POST https://api.withflintpay.com/v1/discount-previews \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "discount": { "promotion": { "promotion_code": "welcome5" } }
  }'

The preview also returns automatic promotions the cart just misses, with the gap, so you can show "Add $10.00 for free shipping." Codes count a use when the buyer starts paying and release it if the payment fails. Buy X get Y, category sales, schedules, and subscription renewals are in the promotions guide.

Who Flint is built for

US businesses selling online or in their own app, paid in USD, that want promotions, orders, catalog, stock, and checkout behind one API. See Flint for online stores.

Stripe Checkout and Flint discounts, side by side

Stripe Checkout SessionsFlint checkout and orders
Discounts per checkoutOne coupon or promotion codeUp to your per-order maximum, 2 by default, up to 10
Buyer enters a second codeYour own cart page, merged into one couponAdd another code on the hosted page
Automatic offersYour server picks a coupon, which uses the one slotredemption_type: "automatic"
Which offers combineYour codecombines_with, exclusivity, stacking_mode
Why a code didn't applyYour codereason on an error or warning
Use limits on stacked codesYour own countermax_uses per code and per promotion
Free shipping over a thresholdA coupon, in the same one slotA shipping-charge promotion that stacks with codes

Questions, answered

Can you use multiple coupons in Stripe Checkout?

No. A Checkout Session supports up to one coupon or promotion code. The discounts parameter takes one entry, and with promotion codes enabled the buyer can redeem one code. To combine offers, merge them into a single coupon on your server before you create the session.

What is the difference between a Stripe coupon and a promotion code?

A coupon is the discount itself: a percentage or amount off, a duration, optional product limits, and redemption limits. Your server applies it. A promotion code is a customer-facing code, such as SPRING10, that points at one coupon and can add restrictions: a specific customer, first-time customers only, a minimum amount, an expiry, and a redemption cap. Many promotion codes can point at the same coupon.

Can Stripe stack discounts anywhere?

Yes, outside Checkout Sessions. Stripe allows up to 20 discounts on a subscription, subscription item, or invoice, applied in order. A Checkout Session, including one that starts a subscription, still takes one coupon or promotion code.

How do I apply a discount automatically in Stripe Checkout?

Your server decides whether the cart qualifies and passes that coupon in discounts when it creates the session. That uses the session's one discount, so the buyer can't add a code on top.

Which checkout lets buyers stack promo codes?

Flint's hosted checkout has an Add another code control, and Flint's promotions engine decides what stacks: automatic promotions and codes are ranked by value, and combines_with, exclusivity groups, stop_after, and a per-order maximum (2 by default, up to 10) set the rules. A code that loses comes back with a machine-readable reason. Card processing runs on Stripe underneath.

Is Flint a good alternative to Stripe Checkout for promotions?

Yes, for US businesses selling online or in an app in USD that want codes, automatic offers, buy X get Y, and free shipping rules without writing a discount engine. Promotions are included in Flint's core pricing with no add-on fee.

Sources

All sources accessed October 15, 2026.