Stack card-linked offers with promo codes
Banks and BNPL providers often have card-linked offers (cashback, accelerated rewards, statement credits) that overlay on top of merchant promo codes. Stacking them — or deciding NOT to stack them when the merchant’s rules disallow it — is a real integration concern for bank, fintech, and brand-loyalty destinations.
This recipe shows the canonical pattern and the conflict checks to put in place.
The two layers
| Layer | Owner | Where it applies |
|---|---|---|
| Merchant promo codes | The merchant — codes apply at the merchant’s checkout rules | Inside Firmly’s Promotions API |
| Card-linked offers | The bank or wallet — applied at the network or PSP layer | Outside Firmly — calculated by the bank when the transaction settles |
Because they live at different layers, both can apply simultaneously if the merchant permits it. Most merchants do; some explicitly exclude card-linked offers from stacking with codes.
The recommended sequence
- Build the cart as usual — discovery, add line items
- Apply merchant promo code first via Firmly’s
add-promo-codes(the reference-page name forPOST /cart/promo-codes) - Re-read the cart and confirm the discount actually applied (200 doesn’t mean applied — see Common issues). Not every promo moves
sub_total— free-shipping and order-level discounts land ontotalor a discount field instead, so check the field the promo actually affects. - Calculate the card-linked offer separately on the destination side, against the post-merchant-promo cart total
- Surface BOTH to the user before checkout: "$5 off with code SAVE5, plus 4% back via your bank card — total saved: $9.20."
- Place the order with the chosen payment instrument
- Reconcile the card-linked offer separately when the bank’s settlement runs (this happens outside Firmly)
// 1-2. Add merchant promo (POST /cart/promo-codes)const cartAfterPromo = await callFirmly('/cart/promo-codes', {promo_codes: ['SAVE5'],});// 3. Did it apply? Compare totals before and after.// NOTE: sub_total only moves for line-item discounts. Free-shipping and// order-level promos land on `total` (or a discount field) instead — compare// the field the promo actually affects, and/or inspect `coupons` /// `cart_discount` on the cart response.const subTotalBefore = cartBeforePromo.sub_total.value; // decimal dollarsconst subTotalAfter = cartAfterPromo.sub_total.value; // decimal dollarsconst totalBefore = cartBeforePromo.total.value;const totalAfter = cartAfterPromo.total.value;const promoApplied = subTotalAfter < subTotalBefore || totalAfter < totalBefore;if (!promoApplied) {// Check notices for the reasonconst promoNotice = cartAfterPromo.notices?.find(n => n.code === 'PROMO_EXPIRED' || n.code === 'PROMO_NOT_APPLICABLE');agent.say(`The code didn't apply — ${promoNotice?.details?.description || 'check the merchant rules'}.`);return;}// 4. Calculate card-linked offer on your side, against the post-promo totalconst cardLinkedOffer = await bankApi.calculateOffer({cardId: user.cardId,merchantDomain: cart.shop_id,cartTotal: cartAfterPromo.total.value,});// 5. Surface both to the user. All math is on `value` (decimal dollars),// so no division is needed. estimatedSavings from the bank is also decimal.const codeSavings = totalBefore - totalAfter;agent.say(`Cart: $${cartBeforePromo.total.value}Code SAVE5: -$${codeSavings.toFixed(2)}Card-linked offer: -$${cardLinkedOffer.estimatedSavings.toFixed(2)} (at settlement)Total to charge today: $${cartAfterPromo.total.value}Estimated savings: $${(codeSavings + cardLinkedOffer.estimatedSavings).toFixed(2)}`);
Conflict checks
Some merchants disallow stacking. Things to validate:
| Check | How |
|---|---|
| Does the merchant allow card-linked offers alongside promo codes? | Merchant-specific; not exposed in the cart response. Talk to Firmly for the merchant’s policy |
| Does the user’s card actually carry the offer for this merchant? | Bank-side check — call your bank’s offer API with the merchant domain |
| Will the offer settle on the card you’re charging? | If the user’s chosen payment method isn’t the offer-bearing card, no card-linked offer applies — surface this |
What can go wrong
- User expects card-linked savings on the wrong card. Make it visually clear which card needs to be charged for the offer to apply.
- Merchant promo code silently rejects the cart. Always re-read and check
cart.noticesforPROMO_EXPIRED,PROMO_NOT_APPLICABLE, etc. - Card-linked offer settles weeks later. Don’t quote it as “savings now” — surface as “estimated, applied at settlement.”
- Double-discounting at the merchant side. Some merchants are sensitive to perceived double-discounting and may reject orders. Confirm with the merchant’s terms.
When NOT to stack
- The merchant’s terms explicitly forbid stacking
- The user has a fixed budget and the agent should pick the SINGLE best offer (often the larger of the two) rather than try both
- The merchant’s order placement fails when both apply (test in sandbox first)