The discount module handles every kind of price reduction — single-use coupon codes, evergreen sale codes, automatic site-wide promotions, BOGO rules, free-shipping thresholds, group benefits. All flow through one calculation endpoint that runs against the cart and returns the line-by-line and total reduction.
Validate and calculate
/storefront/discounts/validateNo auth/storefront/discounts/calculateNo auth/storefront/discounts/automaticNo auth/storefront/discounts/applyNo authvalidate checks whether a code exists, is active, and applies to the current cart — without committing usage. calculate accepts the cart, an optional code, and addresses, and returns subtotal, discount, shipping, tax, and total. This is the single endpoint a checkout UI calls on every cart change. automatic returns the discounts the cart qualifies for without a code — site-wide promos, group-tier discounts, free-shipping thresholds. apply records that a code was used (decrementing usage limits).
const result = await fetch('/api/storefront/discounts/calculate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', orgid: ORG_ID },
body: JSON.stringify({
cart: cartObject,
code: 'SPRING20',
shippingAddress: { country: 'US', state: 'CA', zip: '94110' },
customerId: customerId,
}),
}).then(r => r.json());
// {
// subtotal: 200,
// discount: 40,
// shipping: 8.99,
// tax: 14.4,
// total: 183.39,
// appliedDiscounts: [{ code: 'SPRING20', amount: 40, type: 'percentage' }]
// }
/storefront/discounts/promotions/activeNo authReturns the live promo banners — the discounts you'd display on a homepage strip ("Free shipping over $50"). No cart needed.
Discount CRUD
/storefront/discountsJWT/storefront/discountsJWT/storefront/discounts/:identifierJWT/storefront/discounts/:identifierJWT/storefront/discounts/:identifierJWT{
"name": "Spring Sale 20%",
"code": "SPRING20",
"type": "percentage",
"value": 20,
"scope": "cart",
"minSubtotal": 50,
"maxRedemptions": 1000,
"perCustomerLimit": 1,
"startsAt": "2026-04-01T00:00:00Z",
"endsAt": "2026-04-30T23:59:59Z",
"stackable": false,
"eligibility": {
"tags": ["vip"],
"categories": ["spring-collection"],
"excludeCategories": ["clearance"]
}
}
The type enum: percentage, fixed, free-shipping, bogo, bundle, tiered. scope is cart, line-item, or shipping. eligibility filters by customer tags, product categories, customer-segment IDs, or merchant-account flags.
Activation and lifecycle
/storefront/discounts/:identifier/activateJWT/storefront/discounts/:identifier/deactivateJWTA deactivated discount fails validate immediately with inactive. The record stays for historical reporting.
Usage tracking and reversal
/storefront/discounts/:identifier/usageJWT/storefront/discounts/:identifier/reverseJWTusage increments the redemption counter (called automatically by checkout). reverse decrements it — used when an order is cancelled before fulfillment so the customer's per-customer-limit doesn't burn the slot.
Statistics
/storefront/discounts/statsJWTPer-discount counts: total redemptions, gross uplift, average order value with the discount, conversion rate. Useful for deciding whether a code is paying for itself.
Stacking rules
By default, discounts don't stack. Set stackable: true and the calculation engine combines compatible discounts (percentage + free-shipping is fine; two percentage codes is not, by default). The order is fixed: line-item discounts first, then cart-level percentages, then fixed amounts, then free-shipping.
// Two stackable discounts apply
{
"appliedDiscounts": [
{ "code": "FREESHIP", "type": "free-shipping", "amount": 8.99 },
{ "code": "SPRING20", "type": "percentage", "amount": 40 }
]
}
To restrict combinations, use the mutuallyExclusiveWith: [<discountId>] array on the discount record.
Customer-specific codes
Generate single-use unique codes for individual customers via POST /storefront/discounts with oneTimeUse: true and assignedTo: <customerId>. The code only validates for that customer's cart and burns on first use. Use this for win-back emails, support gestures, and influencer attribution.
Storefront pricing always re-runs discounts/calculate server-side at checkout, regardless of what the client computed. The client preview is for UX only — never trust client-computed totals.
Eligibility against group benefits
If a customer is enrolled in a group/benefit programme via the client account (e.g. "students get 10% off"), the calculate endpoint folds the benefit into the same response. The benefit appears under appliedDiscounts with source: "benefit" so the UI can label it differently.