Skip to content

Checkout and coupons

How loyalty appears in the cart, and how a coupon is held and then spent.


We do not touch your cart. We tell you what a member holds and record what they spent; your checkout does the arithmetic and owns the final price.

UsYou
Which coupons this member hasApplying the discount to the cart
Whether a coupon is valid right nowShowing it in the cart UI
Holding a coupon so it is not double-spentDeciding what the customer pays
Recording redemption when the order landsTax, shipping, rounding

Checkout mockup: a cart with one line item, an applied WELCOME15 coupon showing is_ok: true, and a totals block going from 499,00 subtotal to 424,15 total after a 74,85 discount

The numbers are the exact ones from the line-item example below — 499,00 minus a 15% (74,85) discount is 424,15. We confirmed the coupon was reservable; your checkout did the subtraction.

sequenceDiagram autonumber actor C as Customer participant S as Your server participant D as Diller API participant PSP as Payment provider Note over C,S: Customer is logged in and linked C->>S: Opens the cart S->>D: GET /members/{id}/coupons/ D-->>S: [ coupons ] S-->>C: Cart with available coupons C->>S: Applies WELCOME15 S->>D: GET /members/{id}/coupons/WELCOME15/validate alt Valid D-->>S: { is_ok: true } S-->>C: Discount shown else Not valid D-->>S: { is_ok: false, details: "..." } S-->>C: Explain why end C->>S: Proceeds to payment S->>D: POST /members/{id}/coupons/WELCOME15/reserve<br/>{ external_transaction_id: "ORDER-1001" } D-->>S: { is_ok: true } Note over D: Coupon is held.<br/>It cannot be used elsewhere. S->>PSP: Take payment alt Payment succeeds PSP-->>S: Approved S->>D: POST /members/{id}/transactions<br/>{ external_id: "ORDER-1001", coupon_codes: ["WELCOME15"] } D-->>S: { is_ok: true } Note over D: Redemption recorded,<br/>points awarded,<br/>reservation released. S-->>C: Order confirmed else Payment fails or the cart is abandoned PSP-->>S: Declined, or no answer at all S->>D: POST /members/{id}/coupons/WELCOME15/cancel-reservation D-->>S: { is_ok: true } Note over D: Coupon released,<br/>usable again. S-->>C: Payment failed end

Posting the transaction releases the reservation for you. The one you have to release yourself is the order that never completes — and the hardest case is the customer who simply leaves, because nothing tells you they have.


GET /api/v2.0/stores/{storeId}/members/{memberId}/coupons/
[
{
"id": 0,
"external_ids": [],
"code": "WELCOME15",
"title": "15% off your first order",
"description": "Welcome to the club",
"valid_from": "2026-01-01T00:00:00",
"valid_to": "2026-12-31T23:59:59",
"type": "Welcome",
"discount_type": "Percentage",
"discount_value": 15.0,
"max_redemptions": 1,
"times_redeemed": 0,
"product_ids": [],
"image_url": null
}
]
  • max_redemptions - times_redeemed is how many uses are left. Zero means spent — hide it or grey it out.
  • max_redemptions is never null. On a member’s coupon list, an unlimited coupon carries 10000000. Treat that as unlimited and show no count, or the customer reads “9,999,999 uses left”.
  • product_ids non-empty means the coupon only applies to those SKUs. An empty array means the whole order.
  • Check valid_from / valid_to against the store’s timezone (time_zone on the store object), not your server’s. The loyalty panel recipe shows how.

Before you show a discount as applied, confirm the member may actually use it right now.

GET /api/v2.0/stores/{storeId}/members/{memberId}/coupons/{couponCode}/validate
{ "is_ok": true, "details": "" }

is_ok: false comes with a reason in details — show it rather than a generic failure.

Validate at two moments: when the customer applies the coupon, and again immediately before payment. Carts sit open for a long time and a coupon can expire in between.


A reservation holds a coupon against one pending order so it cannot be spent twice — in another tab, on another device, or in a physical store.

POST /api/v2.0/stores/{storeId}/members/{memberId}/coupons/{couponCode}/reserve
{
"channel": "OnlineStore",
"external_product_id": "SKU-123",
"external_transaction_id": "ORDER-1001"
}
FieldNotes
external_transaction_idRequired. Your order id. The same one you will post as the transaction’s external_id
channel"OnlineStore"
external_product_idOptional — for a coupon tied to one line

Reserve immediately before taking payment, not when the coupon is applied. A reservation does not expire, so one made when the customer first clicks “apply” is held for as long as they browse — and for good if they leave.


POST /api/v2.0/stores/{storeId}/members/{memberId}/coupons/{couponCode}/cancel-reservation
{ "external_transaction_id": "ORDER-1001" }

What releases a reservation:

OutcomeWhat to do
Order completedNothing — posting the transaction releases it
Order cancelled after postingNothing — cancelling the transaction releases it
Payment failedCall cancel-reservation
Cart abandoned at the payment stepCall cancel-reservation — this is the one that leaks

A failed payment usually comes back to you as a callback or a redirect, so releasing there is easy. An abandoned cart sends you nothing. Keep a record of each reservation, and sweep up the ones whose order never arrived:

// Run every few minutes.
async function releaseAbandonedReservations() {
const stale = await db.reservations.findOpenOlderThan({ minutes: 30 });
for (const r of stale) {
if (await orderWasPosted(r.orderId)) continue;
await cancelCouponReservation(r.memberId, r.couponCode, r.orderId);
await db.reservations.markReleased(r.id);
}
}

Pick the age to match how long your payment provider keeps a session open.

Calling cancel-reservation on a reservation that is already gone does no harm: it returns is_ok: false and changes nothing. So when in doubt, release.


stateDiagram-v2 [*] --> Available: Coupon issued to member Available --> Reserved: POST .../reserve<br/>{ external_transaction_id } Reserved --> Redeemed: POST .../transactions<br/>with coupon_codes<br/>(releases the hold) Reserved --> Available: POST .../cancel-reservation Redeemed --> Available: Transaction cancelled<br/>(reverses redemption) Redeemed --> [*]: max_redemptions reached note right of Reserved Held against one order. Not usable anywhere else. Never expires on its own. end note

Redemption happens as part of the transaction, not as a separate call. Put the codes on the order:

{
"external_id": "ORDER-1001",
"coupon_codes": ["WELCOME15"],
"details": [ "..." ]
}

Coupons may also be attached to a single line item:

"details": [
{
"product": { "sku": "SKU-123", "name": "T-shirt" },
"quantity": 1,
"unit_price": 499.00,
"total_price": 424.15,
"discount": 74.85,
"coupon_codes": ["WELCOME15"]
}
]

Use the line-item form when the coupon applies to one product, and the order-level form when it applies to the basket. Do not put the same code in both — it will be counted twice.

If a coupon is used more than once on an order, repeat the code:

"coupon_codes": ["STAMP5", "STAMP5"]

Point-redemption coupons go in a different field

Section titled “Point-redemption coupons go in a different field”

If the merchant runs points-as-money rewards, those redemptions go in coupons rather than coupon_codes, because we need to know how much was spent:

"coupon_codes": ["WELCOME15"],
"coupons": [ { "code": "PTS500", "redeemed_value": 50.00 } ]

See two coupon arrays.

Full transaction shape: Transactions.


Show the member their balance from membership_detail.points. remaining_points is something else — the points still needed for the next tier — see Core concepts.

Points-based coupons appear in the member’s coupon list like any other, so if the merchant runs points-for-currency rewards, you do not need to compute anything — the coupon is already there with its value.

We award points when you post the transaction. Never add them yourself, and do not predict the award in the UI unless the merchant has confirmed the earn rate — points_system.currency_points_ratio on the store object is the input, but level bonuses and excluded products can change the result.


Stamp cards work the same way: reserve, then record on the transaction.

POST /api/v2.0/stores/{storeId}/members/{memberId}/stamp-cards/reserve
{
"reserved_stamps": [
{ "stamp_card_id": 12, "external_product_id": "SKU-123", "count": 1 }
],
"channel": "OnlineStore",
"external_transaction_id": "ORDER-1001"
}

Returns 204 No Content.

Release with:

POST /api/v2.0/stores/{storeId}/members/{memberId}/stamp-cards/cancel-reservation
{
"reserved_stamps": [ { "stamp_card_id": 12, "count": 1 } ],
"external_transaction_id": "ORDER-1001"
}

MistakeWhat the customer sees
No release for abandoned cartsCoupons silently disappear forever
Reserving when applied, not before paymentCoupons locked while browsing
Showing max_redemptions - times_redeemed for an unlimited coupon“9,999,999 uses left”
Using coupon id instead of code404 on validate and reserve
Coupon at both order and line levelDouble-counted redemption
Trusting a member id from the browserOne customer spends another’s coupons
Ignoring times_redeemedSpent coupons offered, then rejected at checkout