GuidesDiscounts & coupons

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.

FieldMeaning
codeThe redeemable string a shopper types.
kind"percent" or "fixed".
valueFor percent: points off the subtotal, 1..100. For fixed: integer cents off the subtotal.
max_redemptionsThe lifetime redemption cap, or null for unlimited.
times_redeemedHow many placed orders have consumed it (read-only).
activeWhether 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:

CodeStatusMeaning
coupon_not_found404No coupon with that code for this tenant.
coupon_inactive412The coupon exists but is not active.
coupon_exhausted412times_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.