Redeem a customer QR
The customer picks a reward in their app — a voucher, an item bought with points, or a stamp-card reward — and shows a QR. Your till validates it, applies the discount and completes it.
customer shows reward QR ──► POST /qr/validate ──► charge bill − discount ──► POST /transactions/redeem (amount = full bill)
Scope required: transactions.write.
Step 1: Validate the QR
curl -X POST "https://staging.app.rewards.network/api/partner/v1/qr/validate" \
-H "Authorization: Bearer rn_live_..." \
-H "Content-Type: application/json" \
-d '{ "token": "RDM-7K2M9Q" }'
The token is whatever the QR contains. The customer can also read you the short code printed under it (RDM-XXXXXX); it is the same redemption. When the token proves the customer showed it — the typed code, or a QR that carries the code (every QR the app shows does) — the answer carries it as redemption_code; keep it for Step 2. Typed codes that name no redemption at this merchant are capped: after 30 within 10 minutes (typos count too) every typed code from your integration at this merchant answers 429 (error_code: redeem.code_attempts) until the window ends; scanned QRs are not affected.
{
"success": true,
"data": {
"qr_type": "redemption",
"customer_id": 123,
"redemption_id": 5512,
"redemption_type": "VOUCHER",
"redemption_code": "RDM-7K2M9Q",
"metadata": {
"reward_name": "10% off any coffee",
"discount_amount": 0,
"discount_percent": 10,
"points_required": 0,
"points_used": 0,
"stamps_required": 0,
"min_purchase_amount": 5,
"max_discount_amount": 3,
"customer_name": "Jane Doe",
"allowed_branch_ids": null,
"campaign_end_date": "2026-12-31"
},
"customer": { "user_id": 123, "full_name": "Jane Doe", "email": "jane@example.com", "is_member": true }
}
}
redemption_type and metadata tell the till what to apply:
redemption_type |
What the customer gets | Fields to use |
|---|---|---|
VOUCHER |
A discount on this sale. | discount_percent or discount_amount, capped by max_discount_amount, only if the sale ≥ min_purchase_amount. |
POINTS |
An item paid with points, or a points-based discount. | reward_name, points_required / points_used. |
STAMPS |
The stamp-card reward. | reward_name, stamps_required. |
allowed_branch_ids is null when the reward is valid at all branches; otherwise only the listed branches may complete it.
A QR that cannot be used answers 400 with a plain-English message you can show the cashier: QR already used or expired, QR code expired, Campaign has expired, Redemption code not found, This reward can only be claimed at Owner Cafe.
Step 2: Complete the redemption
Once the customer has paid, send the bill total before the discount; the server takes the discount off itself:
curl -X POST "https://staging.app.rewards.network/api/partner/v1/transactions/redeem" \
-H "Authorization: Bearer rn_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2c9d0a4e-1b3f-4d5e-9a8b-7c6d5e4f3a2b" \
-d '{
"redemption_id": 5512,
"redemption_code": "RDM-7K2M9Q",
"branch_id": 45,
"amount": 27.00,
"qr_scan_id": "POS-0001-20260922-0043"
}'
| Field | Required | Notes |
|---|---|---|
redemption_id |
yes | From qr/validate. |
redemption_code |
while approval is on | From qr/validate, or read out by the customer. Required while customer approval is on (approval_required in redeem-options) for a redemption the customer made in the app: the id alone does not show that they are at the till. Not needed for one your till created while approval was on (its approval or PIN is the proof). |
branch_id |
yes | Where it is redeemed. |
amount |
yes | The bill total before the discount (0 for a free item with no purchase). The server checks min_purchase_amount against it, subtracts the discount and awards points on what is left (final_amount) if a loyalty campaign is running. Do not send the already-discounted total: the discount would be taken off twice. |
qr_scan_id |
recommended | Your receipt reference. |
{
"success": true,
"message": "Redemption finalized successfully",
"data": {
"transaction": { "id": 9902, "transaction_type": "redeem", "final_amount": "24.30", "points_delta": -0, "...": "..." },
"redemption": { "redemption_id": 5512, "status": "COMPLETED", "redeemed_at": "2026-09-22T10:52:19.000000Z", "...": "..." },
"redemption_type": "VOUCHER",
"reward_name": "10% off any coffee",
"discount_amount": 2.7,
"campaign_end_date": "2026-12-31",
"customer": { "full_name": "Jane Doe", "user_id": 123 }
}
}
While approval is on, a redemption made in the app without its code (or with another) answers 400 with error_code: redeem.code_required / redeem.code_mismatch; nothing is deducted. Ask the customer for the code under their QR and send the redeem again with a new Idempotency-Key.
The customer's points or stamps are deducted here, not at validation. A redemption that was already completed answers success: false with Redemption is already COMPLETED (400); a repeat with the same qr_scan_id answers Redemption already processed with the original data. An id that already belongs to another transaction — for example the purchase commit of the same sale — answers success: false (400) with data.reason: "QR_SCAN_ID_REUSED": nothing is deducted and the QR stays pending, so send the redeem again with its own id.
Check status later
GET /redemptions/{redemptionId} returns the redemption with its status (PENDING, COMPLETED, EXPIRED, CANCELLED). Useful for reconciliation or when the POS lost the response.
No phone?
If the customer created the reward in the app but left the phone at home, or has no app at all, use the cashier-initiated flow: Redeem without the phone.
Webhook
Completion fires redemption.completed. Webhooks.
Reward Networks