Modules
Promotions
Automatic discounts and codes. Applied server-side at checkout, previewable in the cart.
Promotions are applied by the server, at checkout, whether or not your site knows they exist. There is one endpoint here, and it is not for applying discounts. It is for showing the buyer, before they pay, what the server is going to do.
Automatic and coded
An automatic promotion needs nothing from the buyer. Ten percent off, happy hour, buy one get one: it holds because the cart matches the conditions your customer set.
A coded promotion needs a code, typed into a field you provide and passed as discount_code at checkout. Everything else about it is the same.
Your customer configures both in the dashboard: the effect, who it applies to, when it runs and how often it can be used.
| Configured | Options |
|---|---|
| Effect | percentage off, a fixed amount off, or a bundle deal at item level. |
| Scope | The whole order, specific catalog items, or whole categories. |
| Conditions | A minimum order amount, a first-order-only flag, a start and end date, a recurring window such as weekday afternoons. |
| Limits | A maximum number of redemptions across all buyers. |
| Order | A priority, which decides how several promotions stack. |
Previewing the cart
/api/public/{organizationId}/promotions/previewPublicGiven a cart and an optional code, returns which promotions apply, in what order, and the resulting price.
{
"items": [
{ "content_item_id": "8f2a1c34-...", "quantity": 2 }
],
"code": "WELCOME10"
}Send nothing else. Items are required, up to 100 lines with quantities up to 1000. The code is optional: omit it entirely if the buyer typed nothing.
{
"subtotal": 55.00,
"lines": [
{ "promotion_id": "c81a...", "name": "Happy hour",
"effect_type": "percentage", "value": 10, "discount_amount": 5.50 }
],
"code": { "valid": true },
"total_discount": 5.50,
"final": 49.50
}| Field | Render as |
|---|---|
lines | One discount row each, name on the left and the amount as a deduction. The order is the order they stack in. |
code | { valid: true } means it applied and is already counted in lines. { valid: false } means show "code not valid". null means no code was entered. |
total_discount | The total discount. |
final | What the buyer pays for the items. |
It works on the items subtotal only, so fees, the delivery fee and tax are not in it. That makes it the wrong call to hang a cart on: the quote on Online Ordering runs the same discount engine and returns discount.total, discount.lines and the same verdict on the code, alongside everything else the customer will be charged. Ask the quote whenever the cart changes, and keep this one for what it is good at — checking a code the moment it is typed, before there is a basket worth quoting.
One deliberate blind spot
The preview takes no email address, so first-order-only promotions cannot be evaluated and are skipped. The checkout does have the email and applies them there, so a genuine first-time buyer sees the discount on the payment page and in the final total.
The preview can therefore understate a discount, and never overstate one. That direction is chosen: an endpoint that answered differently for an address that had ordered before would be a way to test whether a given person is a customer of this business, from an unauthenticated request.
At checkout
You do not apply anything. Pass the code the buyer typed as discount_code in the checkout body and let the server work out the rest. Automatic promotions need no mention at all.
{
"items": [ ],
"customer": { },
"discount_code": "WELCOME10"
}When it fails
On an error or an empty response, show no discount and the plain subtotal. A promotion that fails to display costs a little goodwill. A cart that refuses to render costs the order.
The endpoint is rate limited per organization and IP, at 30 requests a minute. Debounce it to cart changes rather than firing on every keystroke in the code field.