Developer Documentation

Point Reservations

How points are held and settled on both sides of the ledger — earned points the shopper must claim, and spent points that stay reversible until the order is invoiced.

Spend Side
Reversible Hold
Invoice-Settled

Reward Reservations (Spending Points)

This guide covers what happens after a shopper redeems a reward with their points. Redeeming no longer settles instantly: it creates a reservation — the points are deducted from the shopper's balance right away, but the redemption stays open until Legion sees the matching invoice from the commerce platform. If no invoice ever arrives, the reservation lapses and the points are automatically refunded.

This is the spend-side counterpart of claimable points (earn-side reservations, covered in the Earning tab above). A shopper first claims earned points into their spendable balance, then spends them on a reward — which creates the reservation described here.


How it works

1Your site Legion (user-x) Commerce / ERP (Sage) 2───────── ─────────────── ───────────────────── 31. Shopper clicks "Claim" ─► POST /api/widget/claim-rewards/redeem 4 ├─ deducts points (reversible hold) 5 └─ returns sku + reservationCode + expiresAt 62. List open/synced holds ─► GET /api/webhooks/claim-rewards/reservations 73. Create a Sage invoice with customerPONo = reservationCode 84. Legion invoice sync ◄─ matches invoice to reservation 9 └─ finalizes: redemption → fulfilled 10 …or, if no invoice arrives before expiresAt: 11 Legion expiry sweep └─ reservation → expired, points refunded

Your integration only changes at steps 2–3: pull reservations from Legion and create the Sage invoice yourself, stamping reservationCode as customerPONo. You do not need the shopper to place a commerce order or auto-insert the code into a cart/PO field. Step 4 is handled entirely by Legion's hourly invoice sync.

Reservation lifecycle

StatusMeaning
pendingReserved. Points already deducted from the balance, but the hold is reversible.
fulfilledFinalized. The invoice sync matched a Sage invoice to the reservation; the deduction is final.
expiredNo invoice arrived before expiresAt. The points were refunded automatically.
cancelledReversed (e.g. order cancelled or refunded). The points were refunded.

Every transition is auditable: the finalizing invoice number, date, and customer are stamped into the redemption record, and the refund on expiry/cancellation appears in the shopper's points history as a refund transaction.


1. Redeeming creates the reservation

POST /api/widget/claim-rewards/redeem works exactly as before — same request, same authentication — but the response now carries two extra fields:

1POST {BASE_URL}/api/widget/claim-rewards/redeem 2Authorization: Bearer {TOKEN} 3Content-Type: application/json 4 5{ "rewardId": "b1f3…" }
1{ 2 "redemptionId": "9c2a…", 3 "transactionId": "4d7e…", 4 "balance": 2500, 5 "currency": "points", 6 "sku": "STORE-CREDIT-100", 7 "reservationCode": "RSV-Y3BZVH3R6HFZ", 8 "expiresAt": "2026-08-21T16:14:03.000Z" 9}
  • reservationCode — a short, unguessable code (RSV- + 12 characters) that uniquely identifies this reservation. It fits within Sage's 30-character customerPONo limit by design.
  • expiresAt — when the unfinalized reservation lapses and the points are refunded. The window is a per-deployment setting (30 days by default).

The shopper's balance returned here already reflects the deduction — display it as-is.


2. List reservations (server-to-server)

GET /api/webhooks/claim-rewards/reservations returns every pending (created, not yet matched) and fulfilled (already matched to an invoice) reservation in the shop. Expired, cancelled, and redemptions that never received a reservation code (commerce-only checkout or flat invoice deductions) are omitted.

GET {BASE_URL}/api/webhooks/claim-rewards/reservations Authorization: Bearer {API_TOKEN}

Mint {API_TOKEN} yourself in the admin dashboard under Settings → API Tokens. The token is scoped to one shop and can be revoked there at any time; the same token authenticates the redeem and invoice-sync webhooks.

Query paramDefaultDescription
statusbothpending or fulfilled. Omit to return both.
userIdRestrict to one shopper.
shopIdtokenOptional; must match the token's shop.
page11-based page.
limit50Page size (max 200).
1{ 2 "reservations": [ 3 { 4 "id": "9c2a…", 5 "userId": "…", 6 "organizationId": null, 7 "status": "pending", 8 "reservationCode": "RSV-Y3BZVH3R6HFZ", 9 "sku": "STORE-CREDIT-100", 10 "rewardId": "b1f3…", 11 "rewardName": "Store Credit $100", 12 "pointsAmount": 1000, 13 "requestedAt": "2026-08-19T18:00:00.000Z", 14 "expiresAt": "2026-09-18T18:00:00.000Z", 15 "fulfilledAt": null, 16 "invoice": null 17 } 18 ], 19 "pagination": { "page": 1, "limit": 50, "total": 1 } 20}

Use this list (or the reservationCode from the redeem response) when you create the Sage invoice. Fulfilled rows include invoice (invoiceNo, invoiceDate, customerNo, itemCode) for audit once the sync has matched.


3. Stamp the reservation code on the Sage invoice

When you send the reward to Sage, set the invoice's customer PO number to the reservationCode. The requirement is simply that the value arrives in Sage as the invoice's customerPONo.

Why it matters. The reservation code is the primary matching key. If it is missing, Legion falls back to matching by shopper + SKU, which works for one open reservation per SKU but cannot disambiguate a shopper with two pending reservations for the same reward. Always stamp the code when you create the invoice.


4. Legion finalizes (or expires) the reservation

You do not call anything for this step. Legion's invoice sync runs hourly:

  1. It pulls new invoices from the ERP (Sage) since the last run.
  2. For each invoice it looks for a pending reservation whose reservationCode equals the invoice's customerPONo (primary match), or — failing that — a pending reservation for the same shopper and an invoiced SKU (fallback match).
  3. Matched reservations become fulfilled; the invoice number, date, and customer are recorded on the redemption for audit.
  4. Reservations past expiresAt become expired and the points are refunded in the same sweep.

Fulfilled and expired reservations are visible to shop staff in the admin console (Points → Redemptions → History), including the reservation code and finalizing invoice number.


Monitoring from your side (optional)

  • Reservations list (GET /api/webhooks/claim-rewards/reservations) is the source of truth for open holds and already-synced invoices.
  • Points history (GET /api/widget/points-history) shows the initial deduction at redeem time and, if the reservation later expires or is cancelled, a refund row re-crediting the points.
  • Balance (GET /api/widget/claim-rewards/balance) always reflects holds: a pending reservation's points are not spendable.
  • Order webhook — if your platform already reports order events to POST /api/webhooks/claim-rewards/redeem, order.cancelled and order.refunded release the hold immediately instead of waiting for expiry.

Integration checklist

  • Redeem response's reservationCode and expiresAt captured.
  • Partner backend lists reservations via GET /api/webhooks/claim-rewards/reservations.
  • Sage invoice created with customerPONo = reservationCode (≤ 30 chars — the code always fits). Shopper does not need to place a commerce order for the hold to exist.
  • Balance shown from the redeem response / balance endpoint (already reflects the hold).
  • No point deduction on your side at invoice time — Legion owns the ledger.
  • (Optional) Cancellations/refunds reported to the redeem webhook for immediate release of the hold.
  • Shoppers informed that unfinalized reservations auto-refund after the expiry window.

Related Documentation

SSO Authentication — every endpoint here requires an SSO bearer token. Read the SSO guide →

Claim Rewards UI — catalog, balance, and redeem endpoints for building a custom rewards experience. Back to the integration guide →