Award points & stamps
The core flow of a loyalty-enabled sale. Three calls: identify the customer, preview, commit.
customer shows QR ──► POST /qr/validate ──► POST /transactions/preview ──► customer pays ──► POST /transactions/commit
Scope required: transactions.write.
Step 1: Identify the customer
Scan the QR shown in the customer's Reward Networks app and send the raw string:
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": "CUST-LOYALTY-ID-RN-000123" }'
{
"success": true,
"data": {
"qr_type": "customer_id",
"customer_id": 123,
"customer": {
"user_id": 123,
"full_name": "Jane Doe",
"email": "jane@example.com",
"is_member": true
}
}
}
qr_type tells you what was scanned:
qr_type |
Meaning | Next call |
|---|---|---|
customer_id |
The customer's identity QR (or their typed code RN-000123). |
transactions/commit |
redemption |
A reward the customer wants to redeem (voucher / points / stamps). | transactions/redeem — see Redeem a customer QR |
network_reward |
A network reward QR (RWD-…). |
rewards/claim — see Rewards, packages, deals & gifts |
If the customer has no phone, look them up by phone number instead: Customers & members.
Step 2: Preview
Tell the customer what they will earn before they pay.
curl -X POST "https://staging.app.rewards.network/api/partner/v1/transactions/preview" \
-H "Authorization: Bearer rn_live_..." \
-H "Content-Type: application/json" \
-d '{ "amount": 48.50, "discount_amount": 5.00, "branch_id": 45 }'
{
"success": true,
"data": {
"loyalty_enabled": true,
"loyalty_running": true,
"stamp_enabled": true,
"stamp_running": true,
"original_amount": 48.5,
"discount_amount": 5,
"final_amount": 43.5,
"points_earned": 43,
"stamps_earned": 1,
"point_campaign_id": 17,
"stamp_campaign_id": 18
}
}
Points are computed on final_amount (amount minus discount). branch_id is optional but recommended: a campaign limited to certain branches only earns there, so a preview with the sale's branch shows exactly what commit will award. When the merchant has no running campaign (at that branch) the call answers success: false with data.reason (NO_ACTIVE_RULES, BRANCH_NOT_IN_CAMPAIGN, …) — treat it as "nothing to earn", not as an error.
Step 3: Commit
After payment succeeded:
curl -X POST "https://staging.app.rewards.network/api/partner/v1/transactions/commit" \
-H "Authorization: Bearer rn_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f6a2c1e-9d4b-4c3a-8e5f-0a1b2c3d4e5f" \
-d '{
"user_id": 123,
"branch_id": 45,
"amount": 48.50,
"discount_amount": 5.00,
"qr_scan_id": "POS-0001-20260922-0042"
}'
| Field | Required | Notes |
|---|---|---|
user_id |
yes | From qr/validate (customer_id) or customers/lookup. Must already be a member (is_member: true), otherwise success: false with data.reason = NOT_A_MEMBER — see Customers & members. |
branch_id |
yes | One of the merchant's branches (GET /branches). Campaigns limited to other branches award nothing here (BRANCH_NOT_IN_CAMPAIGN). |
amount |
yes | Gross amount, > 0. |
discount_amount |
no | Subtracted before points are computed. |
redemption_id |
no | A voucher/points/stamps redemption already applied to this sale. See below. |
qr_scan_id |
recommended | Your own stable reference for this sale (receipt number). |
{
"success": true,
"message": "Transaction committed successfully",
"data": {
"transaction": {
"id": 9901,
"merchant_id": 7,
"user_id": 123,
"branch_id": 45,
"qr_scan_id": "POS-0001-20260922-0042",
"transaction_type": "earn",
"original_amount": "48.50",
"discount_amount": "5.00",
"final_amount": "43.50",
"points_delta": 43,
"stamps_delta": 1,
"source": "partner",
"created_at": "2026-09-22T10:41:07.000000Z"
},
"points_earned": 43,
"stamps_earned": 1,
"loyalty_enabled": true,
"stamp_enabled": true
},
"is_duplicate": false
}
Keep data.transaction.id: it is the transactionId for a void.
Retries and duplicates
Two independent safety nets, use both:
Idempotency-Keyheader — any unique string per attempt group (a UUID). A retry with the same key and the same body returns the stored response with headerIdempotency-Replayed: true. Same key with a different body →422. A retry while the first attempt is still running →409. Errors are stored too: after a4xxsuch as "Select the branch…", send the corrected request with a new key. Keys are remembered for 24 hours and are private to your merchant.qr_scan_idbody field — your receipt reference. If a transaction with this id already exists for the merchant, commit returns it with"is_duplicate": trueinstead of awarding again. This survives longer than the idempotency window. The id is unique per merchant and names one transaction: give the redeem of the same sale its own id (e.g.POS-0001-20260922-0042-R). An id that already belongs to another transaction (a redemption, or another customer's sale) answerssuccess: falsewithdata.reason: "QR_SCAN_ID_REUSED"and awards nothing.
Network timeouts on the POS side should always be retried with the same key and body. Never generate a new qr_scan_id for a retry.
Applying a voucher to the same sale
When the customer redeems a voucher and earns on the remaining amount, do the redemption first, then commit with the redemption_id:
POST /qr/validatewith the voucher QR →qr_type: "redemption", noteredemption_idand the discount inmetadata.- Charge the customer the bill minus that discount (the server applies the same rule).
POST /transactions/redeemwithredemption_id,branch_idandamount= the bill before the discount — the server takes the discount off, completes the voucher and awards points on the amount paid. See Redeem a customer QR.
Only call transactions/commit with redemption_id when your flow completes the redemption separately and you need to link the earn to it.
Webhook
A successful commit fires transaction.committed to every endpoint subscribed to the merchant. Webhooks.
Reward Networks