Discount codes

How discount codes are scoped, the three discount shapes and which of them the admin form can actually create, validity windows, usage limits, and where a discount comes out of your margin.

4 min readUpdated 3 August 2026discounts, coupons, promotions

Discount codes cut the price of one product. That is the first thing to internalise, because it is not how most stores work: a code is created against a product id, and it is rejected on every other product in your catalogue.

There is no store-wide code, no category code, no cart-total threshold and no stacking.

Creating a code

Admin → E-commerce → Sales → Discounts (/admin/ecommerce/discount).

Field Rules
productId Required. The one product this code applies to
code Required, and unique across the entire store
type PERCENTAGE (default), FIXED or FREE_SHIPPING
percentage 0–100. Only meaningful for PERCENTAGE
amount Cannot be negative. Only meaningful for FIXED
maxUses At least 1. Leave empty for unlimited
validFrom Optional. Empty means live immediately
validUntil Required, and must be in the future when you create it
status Active/inactive

The storefront normalises whatever the customer types to uppercase before it looks the code up. A code saved as summer10 is therefore unreachable — the lookup asks for SUMMER10 and finds nothing, and the customer is told the code is invalid. There is no error at creation time and nothing in the admin panel flags it.

Type every code in capitals.

The create and edit forms show the product, code, percentage, valid-until date and status. type, amount, maxUses and validFrom are real, populated columns that the API accepts and the checkout honours, but they have no form field — they appear only in the read-only view panel.

In practice that means the admin screens create percentage discounts. Fixed amounts, free shipping, usage caps and scheduled start dates have to be set through POST /api/admin/ecommerce/discount (the request body accepts all of them and requires code, type, validUntil, productId and status).

The three shapes

Type Effect at checkout
PERCENTAGE Subtotal is reduced by percentage%
FIXED Subtotal is reduced by amount, capped at the subtotal so it can never go negative
FREE_SHIPPING The checkout's shipping fee becomes zero. The subtotal is untouched

A FIXED discount larger than the line is clamped, not carried over — there is no store credit.

FREE_SHIPPING waives the whole checkout's shipping charge, which is charged once regardless of how many products are in the cart. On a digital-only cart it does nothing, because no shipping was charged.

Validity, limits and one-per-customer

Four independent checks stand between a code and a discount:

  1. Active. status must be true.

  2. In window. validUntil must not have passed. validFrom, if set, must have arrived.

  3. Under its cap. If maxUses is set, the number of customers who have consumed it must be below that number. The count is taken inside the checkout transaction with the discount row locked, so two simultaneous checkouts cannot both take the last use.

  4. Not already used by this customer. One use per customer, permanently. There is no per-customer allowance above one.

A single checkout also refuses to apply the same discount to two lines, so a customer cannot split a cart to double a code.

The code-validation endpoint the storefront calls rejects a code whose validFrom is in the future. The checkout's own re-check looks at status, product and validUntil only. Through the normal storefront flow a scheduled code is correctly refused; treat validFrom as a display-and-validation rule rather than a hard lock, and use status if you need a code to be genuinely unusable.

What the customer experiences

  1. They enter the code on the cart or checkout page.
  2. The storefront calls the validation endpoint, which is rate-limited and does not consume the code. It returns the type, the value and whether it is valid, with a specific message when it is not — expired, not yet active, already used, or limit reached.
  3. The discount is applied to the quoted total.
  4. At checkout the same checks run again inside the money transaction. The usage record is written there, and only there.

The two-phase design means a customer who validates a code and abandons the cart has not burnt it.

Where the money comes from

The discount comes out of your revenue, not out of tax or shipping:

  • The buyer is debited subtotal − discount + shipping + tax.
  • Tax is calculated on the discounted subtotal, so a discount reduces the tax charged too.
  • The platform wallet is credited the discounted subtotal as store revenue.
  • Shipping and tax are credited separately as a pass-through.

Refunds follow the same shape. Cancelling a discounted order returns the buyer exactly what left their wallet, and reverses exactly what was credited to you.

Cancelling an order refunds the buyer and restores stock. It does not clear the usage record, so the customer cannot use that code again on a replacement order. Issue a new code if you need them to re-buy at the same price.

Reading a discount row

The list shows the code, its product, the percentage, the valid-until date and the status. That is not enough to understand a FIXED or FREE_SHIPPING code — the percentage column reads 0 and the actual rule is invisible.

Open the view panel. It leads with the rule itself, stated in the terms of the discount's own type, plus the window it is live in, whether that window has already closed, and the usage limit. A discount can be status: true and long expired; the status badge cannot tell you that and the panel can.

Practical notes

  • One code per product, or many. A product can carry several codes. Nothing stops you creating LAUNCH20 and VIP30 against the same item; the customer applies one.
  • Codes are global identifiers. Uniqueness is across the whole store, so you cannot reuse SUMMER on two products. Name them SUMMER-<product> if you run parallel promotions.
  • Deactivate rather than delete. Deleting a discount cascades to its usage records; deactivating leaves the audit trail intact and stops the code dead.
  • You cannot backdate. validUntil is validated as a future date at creation, so a code cannot be created already expired.

Next: Store settings or the API and permissions reference.