Discounts and coupons
Goal: create a coupon, preview what it discounts, and redeem it on an order.
Audience: an operator creating the coupon, and a storefront developer applying it.
Coupon management (create / list / read) lives on the operator surface,
not the fdk_ merchant API and not the storefront token. There is no coupon
CRUD on the storefront token today — a storefront only applies a code that
already exists. This guide documents exactly what ships.
What a coupon is
A coupon is a tenant-scoped row: a code, a kind, a value, and an optional
redemption cap.
| Field | Meaning |
|---|---|
code | The redeemable string a shopper types. |
kind | "percent" or "fixed". |
value | For percent: points off the subtotal, 1..100. For fixed: integer cents off the subtotal. |
max_redemptions | The lifetime redemption cap, or null for unlimited. |
times_redeemed | How many placed orders have consumed it (read-only). |
active | Whether it can still be redeemed. |
A fixed discount is clamped so it can never exceed the subtotal — a
value: 10000 coupon on a $5.00 cart discounts $5.00, not more.
Create a coupon (operator)
The coupon routes live under /v1/tenants/{tenant} and are authenticated with
the operator token (x-operator-token, or a Bearer operator session) — the
same front door the console uses.
Create it
POST /v1/tenants/{tenant}/coupons HTTP/1.1
Host: api.ruo.pro
x-operator-token: <operator token>
Content-Type: application/json
{ "code": "WELCOME10", "kind": "percent", "value": 10, "max_redemptions": 500 }A 201 returns { coupon: { id, code, kind, value, max_redemptions, times_redeemed, active } }. The body is validated: a missing code, a kind
other than percent/fixed, a non-positive value, or a percent value over
100 all fail 400 invalid_body. A duplicate code is 409 code_exists.
List and read
GET /v1/tenants/{tenant}/coupons -> { coupons: [...] }
GET /v1/tenants/{tenant}/coupons/{code} -> { coupon: {...} }An unknown code is 404.
A fixed-amount coupon that takes $5 off looks like
{ "code": "SAVE5", "kind": "fixed", "value": 500 } — value is cents.
Preview the totals (storefront)
Before you place an order, preview exactly what the coupon discounts. The preview runs the identical catalog-price, group-pricing, coupon, and fail-closed shipping/tax legs that placement runs — but writes nothing and consumes no redemption. It is a storefront-token route (call it server-side, where the token lives):
POST /v1/tenants/{tenant}/checkout/preview HTTP/1.1
Host: api.ruo.pro
x-storefront-token: <storefront token>
Content-Type: application/json
{ "items": [{ "variant_id": "var_123", "quantity": 1 }], "coupon_code": "WELCOME10" }{
"subtotal_cents": 4999,
"discount_cents": 500,
"shipping_cents": 500,
"tax_cents": 393,
"total_cents": 5392,
"currency": "USD",
"coupon_code": "WELCOME10"
}Apply it at checkout
Pass coupon_code on the checkout body. The placed order records the applied
code and the resulting discount_cents.
const result = await store.checkout.submit(
{ customer_ref, items, card, coupon_code: "WELCOME10" },
{ idempotencyKey: `cart-${cartId}:attempt-1` },
);Refusal codes — preview and placement agree
A refused coupon returns the same stable code whether you previewed or
placed, so the two never disagree. The SDK throws FlightdeckError; branch on
err.body.error.code:
| Code | Status | Meaning |
|---|---|---|
coupon_not_found | 404 | No coupon with that code for this tenant. |
coupon_inactive | 412 | The coupon exists but is not active. |
coupon_exhausted | 412 | times_redeemed has reached max_redemptions. |
Beyond coupons: customer-group pricing
Coupons are per-order codes. For standing, account-level discounts the operator
surface also manages customer pricing groups (POST /v1/tenants/{tenant}/customer-groups) with a default_discount_bps applied to
a group’s members automatically at pricing time. That group discount and a
coupon both flow through the same server-computed discount_cents.
Related
- Integrate checkout — placing the order the coupon applies to.
- Tenant-facing API — the routes reachable with a credential.
- Money invariants — integer cents and server-authoritative totals.