OfferKitDocs

Combine discounts safely

Combine OfferKit voucher discounts in one atomic redemption, configure stacking categories and limits, and understand which codes apply to the order.

Stacking lets one order use several discount vouchers. OfferKit sorts the codes by priority, applies exclusivity, caps each discount against the running total, and commits the accepted set in one transaction.

Use stacking only when the commercial policy genuinely allows multiple codes. A simpler single-code checkout is easier for customers and support teams to understand.

Define the policy

For every voucher family, decide:

  • whether it may combine with other discounts;
  • its priority relative to other codes;
  • whether it should compete as an exclusive voucher, excluding every other voucher;
  • whether percentage discounts should have a maximum amount;
  • what the customer sees when a code is skipped.

Mark a voucher exclusive when it must win alone. If any submitted eligible voucher is exclusive, only the exclusive voucher with the highest numeric priority applies. Even a higher-priority non-exclusive voucher is skipped. There is no separate “may combine” flag.

Without an exclusive voucher, larger priority numbers run first. Equal priorities use creation time, oldest first, then voucher ID. This ordering is deterministic; it does not choose whichever combination saves the most money.

Preview individual codes

Validate codes as the customer adds them so you can show immediate feedback. The final commit must still use stack redemption; several independent single redemptions are not atomic and can leave a partially discounted order after a failure.

Commit the stack

const result = await offerkit.vouchers.stackRedeem({
  codes: submittedCodes,
  customerExternalId: user.id,
  externalOrderId: order.id,
  order: {
    amount: order.subtotalCents,
    currency: order.currency,
    items: order.items,
  },
  idempotencyKey: `${order.id}:vouchers`,
});

The operation is all-or-nothing. A successful response contains the batch ID, total discount, final order, calculation breakdown, and one redemption entry per committed voucher. A retry with the same idempotency key returns the original batch.

Use the breakdown to explain ordering and skipped effects. Reasons can include exclusivity_lost and zero_after_running_total.

Important constraint

Gift cards are not supported in the discount stack. Treat gift-card spend as a separate stored-value or payment step and define whether it happens before or after promotional discount calculation in your checkout policy.

Test before launch

Test:

  • two and three ordinary discount codes;
  • an exclusive high-priority code;
  • an exclusive low-priority code;
  • fixed and percentage discounts together;
  • discounts that would reduce the running total below zero;
  • one invalid code in an otherwise valid stack;
  • a gift card submitted in the stack;
  • simultaneous requests with the same idempotency key.

Record the returned batch and entry IDs with the order so support can reconstruct the calculation later.

Follow the calculation

On a USD 100 order, put a USD 10 voucher before a 20% voucher:

StepDiscountRemaining amount
Starting order—USD 100.00
Fixed voucherUSD 10.00USD 90.00
20% of the remaining USD 90USD 18.00USD 72.00

Reversing their priorities gives USD 20 off first, then USD 10, leaving USD 70. Two 10% vouchers leave USD 81, not USD 80. Percentage amounts round half up to an integer minor unit, then apply any cap. Every effect is capped at the remaining total.

An effect after the total reaches zero is skipped with zero_after_running_total and has no committed redemption entry. An exclusive winner produces exclusivity_lost for other codes. A validation failure such as an expired code rejects the stack; an ordinary stacking skip is part of a successful calculation. At most 20 codes may be submitted.

Stack redemption has no read-only combined-preview endpoint. Individual validation can explain whether each code is usable, but adding its individual preview amounts does not reproduce the running-total calculation. Use the committed breakdown as the accepted result and keep your displayed calculation consistent with this ordering.