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. Add SKU to cart, stamp reservationCode on the order ──► order placed
7 (customerPONo = code)
83. Order invoiced in Sage ◄────────────────────────────── invoice created
94. Legion invoice sync ◄─ matches invoice to reservation
10 └─ finalizes: redemption → fulfilled
11 …or, if no invoice arrives before expiresAt:
12 Legion expiry sweep └─ reservation → expired, points refundedYour integration only changes at step 2: alongside adding the returned
sku to the cart, stamp the returned reservationCode on the order so it
reaches your ERP as the customer PO number (customerPONo in Sage). Steps 3–4
are 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. No UI changes are required beyond (optionally) surfacing the reservation code on the order confirmation.
When you add the reward sku to the cart and the shopper checks out, set the
order's customer PO number to the reservationCode. How you do this
depends on your platform; the requirement is simply that the value arrives in
Sage as the invoice's customerPONo.
1const { sku, reservationCode } = await redeemReward(token, reward.id);
2
3// WooCommerce (legion-sso plugin) — pass it through to the order meta:
4await window.legionAuth.addToCartBySku(sku, {
5 quantity: 1,
6 rewardId: reward.id,
7 purchaseOrder: reservationCode, // becomes customerPONo in Sage
8});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 your platform allows it.
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/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.customerPONo (≤ 30 chars — the code always fits).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 →