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
| Type | Value means | Example |
|---|---|---|
percentage | Percent off the qualifying subtotal | 10 = 10% off |
fixed_amount | Minor units off | 500 = $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
| Field | Effect |
|---|---|
startsAt / endsAt | Validity window. Outside it, the code is rejected |
usageLimit | Total redemptions across all buyers |
maxRedemptionsPerCustomer | Redemptions by one customer |
isActive | Off switch that keeps the record and its history |
conditions.minSubtotal | Minimum order total to qualify |
conditions.productIds | Restrict 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_shippingorbuy_x_get_yrow) - 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.productIdsagainst 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