Discounts

Percentage and fixed-amount promotion codes: the limits you can put on them, the nine reasons a code gets rejected, and how product scoping matches.

A discount is a code a buyer types at checkout. Throttle computes the discounted total before handing the final amount to payment — the processor never sees the code or does any of the arithmetic.

Two types

TypeValue meansExample
percentagePercent off the qualifying subtotal10 = 10% off
fixed_amountMinor units off500 = $5.00 off

Free shipping and buy-X-get-Y were retired. Creating one is rejected, and a legacy row that still exists fails validation loudly rather than applying a silent $0 discount while consuming a redemption.

Limits you can set

FieldEffect
startsAt / endsAtValidity window. Outside it, the code is rejected
usageLimitTotal redemptions across all buyers
maxRedemptionsPerCustomerRedemptions by one customer
isActiveOff switch that keeps the record and its history
conditions.minSubtotalMinimum order total to qualify
conditions.productIdsRestrict the discount to certain items

usageCount increments on redemption, so a code with usageLimit: 100 stops working on the 101st attempt rather than going negative.

Why a code gets rejected

Validation returns a specific message, and it is worth surfacing the real one to the buyer rather than a generic “invalid code”:

  • The code does not exist
  • The discount is inactive
  • The type is no longer supported (a retired free_shipping or buy_x_get_y row)
  • It has not started yet
  • It has expired
  • The total usage limit is reached
  • This customer’s limit is reached
  • The order is below the minimum total

“Expired” and “you need $20 more” are different problems for the buyer, and only one of them is recoverable in the moment.

Product scoping without a product catalog

Throttle stores no products, so conditions.productIds cannot refer to a catalog. It matches against whatever identifiers your line items carry — the line item id, referenceId, sku, productId, variantId, or the same keys inside line-item metadata. Matching is case-insensitive and trimmed.

This is forgiving by design, and that cuts both ways. Because it will match a SKU, a product id, or a variant id interchangeably, a scoped discount silently applies to nothing if the identifiers you listed don’t appear on any line item — the code validates, the discount computes to zero on the qualifying subtotal. When a scoped discount “doesn’t work,” compare the ids in conditions.productIds against what your cart payload actually sends.

When a product scope is set, the discount applies only to the matching lines’ subtotal, not to the whole order.

Codes are per environment

The uniqueness constraint on a code includes the environment, so WELCOME10 in your test environment and WELCOME10 in production are different records with separate usage counts. Creating a code in test does not create it live.

Redemption is recorded

Each successful application writes a redemption row tying the discount to the order and customer. That is what enforces per-customer limits, and it is also the audit trail when someone asks who used a code and when.

Last updated August 9, 2026