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.
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 coupon | Stripe promotion code |
|---|
| Who applies it | Your server, in discounts | The buyer, in the code field |
| Restrict to one customer | No | Yes |
| First-time customers only | No | Yes |
| Minimum order amount | No | Yes |
| Limit to some products | applies_to.products | Inherited from its coupon |
| Per Checkout Session | One 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:
// 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
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 -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:
{
"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 -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):
{
"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 -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 -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 Sessions | Flint checkout and orders |
|---|
| Discounts per checkout | One coupon or promotion code | Up to your per-order maximum, 2 by default, up to 10 |
| Buyer enters a second code | Your own cart page, merged into one coupon | Add another code on the hosted page |
| Automatic offers | Your server picks a coupon, which uses the one slot | redemption_type: "automatic" |
| Which offers combine | Your code | combines_with, exclusivity, stacking_mode |
| Why a code didn't apply | Your code | reason on an error or warning |
| Use limits on stacked codes | Your own counter | max_uses per code and per promotion |
| Free shipping over a threshold | A coupon, in the same one slot | A 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.