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.
1. Earn
The shopper completes a campaign. Points are granted as a pending reservation (XP accrues immediately).
2. Claim
The shopper claims pending points, converting them into spendable balance on the ledger.
3. Spend
Redeeming a reward places a hold: points are deducted, and a reservation code ties it to the order.
4. Finalize
Legion's invoice sync matches the ERP invoice and finalizes the deduction — or auto-refunds on expiry.
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.
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 refundedYour 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.
| Status | Meaning |
|---|---|
pending | Reserved. Points already deducted from the balance, but the hold is reversible. |
fulfilled | Finalized. The invoice sync matched a Sage invoice to the reservation; the deduction is final. |
expired | No invoice arrived before expiresAt. The points were refunded automatically. |
cancelled | Reversed (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.
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.
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 param | Default | Description |
|---|---|---|
status | both | pending or fulfilled. Omit to return both. |
userId | — | Restrict to one shopper. |
shopId | token | Optional; must match the token's shop. |
page | 1 | 1-based page. |
limit | 50 | Page 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.
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.
You do not call anything for this step. Legion's invoice sync runs hourly:
reservationCode equals the invoice's customerPONo (primary match), or —
failing that — a pending reservation for the same shopper and an invoiced
SKU (fallback match).fulfilled; the invoice number, date, and
customer are recorded on the redemption for audit.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.
GET /api/webhooks/claim-rewards/reservations) is
the source of truth for open holds and already-synced invoices.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.GET /api/widget/claim-rewards/balance) always reflects holds:
a pending reservation's points are not spendable.POST /api/webhooks/claim-rewards/redeem, order.cancelled and
order.refunded release the hold immediately instead of waiting for expiry.reservationCode and expiresAt captured.GET /api/webhooks/claim-rewards/reservations.customerPONo = reservationCode
(≤ 30 chars — the code always fits). Shopper does not need to place
a commerce order for the hold to exist.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 →