Webhooks
Get an HTTPS POST when something happens at a merchant you are linked to, instead of polling the history endpoints. Register endpoints in the Developer Portal → Webhooks.
Events
| Event | Fired when | data fields |
|---|---|---|
transaction.committed |
Points/stamps awarded for a purchase — by you, another partner, or the merchant app | transaction_id, user_id, branch_id, final_amount, points_earned, stamps_earned, qr_scan_id, source |
transaction.voided |
An earn was voided | transaction_id, reversal_transaction_id, user_id, developer_id, points_reversed, stamps_reversed |
redemption.completed |
A voucher / points / stamps redemption was completed | redemption_id, user_id, branch_id, redemption_type, points_delta, stamps_delta |
redemption.approved |
The customer approved a cashier redemption in the app (Customer approval); complete it before expires_at |
redemption_id, redemption_code, user_id, redemption_type, expires_at |
redemption.declined |
The customer declined a cashier redemption; it is cancelled | redemption_id, redemption_code, user_id, redemption_type |
membership.created |
A customer joined the merchant | membership_id, user_id, branch_id |
reward.claimed |
A network reward QR (RWD-) was claimed at the merchant |
network_redemption_id, reward_id, user_id, branch_id, purchase_amount, discount_amount, final_amount |
package.redeemed |
A package voucher (PKG- / APT-) was redeemed |
voucher_code, seq, user_package_id, package_id, user_id, branch_id, remaining |
item_deal.redeemed |
A Deal Hunt order (DEAL-) was redeemed |
order_id, code, deal_id, qty, user_id, branch_id |
birthday_gift.redeemed |
A birthday gift voucher (BDAY-) was redeemed |
claim_id, code, campaign_id, reward_label, user_id, branch_id |
The last four fire whoever redeemed the code — your till, another partner or the merchant app.
Pick the events per endpoint when you create it. New event types are never added to an existing endpoint on their own: edit the endpoint in the portal to subscribe. Events are per merchant, not per key: if you are linked to three merchants you receive all three merchants' events on the same endpoint, your sandbox merchant included. The top-level merchant_id says which merchant an event belongs to and is_sandbox whether it came from your sandbox merchant.
Delivery
POST /webhooks/reward-networks HTTP/1.1
Content-Type: application/json
X-RN-Event: transaction.committed
X-RN-Signature: t=1758537667,v1=5f1d4c...e9
{
"id": "0c1a3d2e-6b7f-4a8c-9d0e-1f2a3b4c5d6e",
"event": "transaction.committed",
"created_at": "2026-09-22T10:41:07+00:00",
"merchant_id": 17,
"is_sandbox": false,
"data": {
"transaction_id": 9901,
"user_id": 123,
"branch_id": 45,
"final_amount": 43.5,
"points_earned": 43,
"stamps_earned": 1,
"qr_scan_id": "POS-0001-20260922-0042",
"source": "partner"
}
}
Respond with any 2xx within 10 seconds. Do the work after responding if it is slow.
id is unique per event. Deliveries can repeat (retries, or the same event to several endpoints), so store the id and ignore duplicates.
Verify the signature
Every delivery is signed with the whsec_… secret shown once when you created the endpoint. Reject anything that fails.
X-RN-Signature: t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
// PHP
[$t, $v1] = explode(',', $request->header('X-RN-Signature'));
$timestamp = substr($t, 2);
$expected = hash_hmac('sha256', $timestamp . '.' . $request->getContent(), $secret);
$valid = hash_equals($expected, substr($v1, 3)) && abs(time() - (int) $timestamp) < 300;
// Node.js
const [t, v1] = req.headers['x-rn-signature'].split(',');
const timestamp = t.slice(2);
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1.slice(3)))
&& Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
Use the raw request body, not a re-serialised one. Reject timestamps older than 5 minutes to block replays.
Retries and disabling
| Attempt | Delay after failure |
|---|---|
| 1 → 2 | 1 minute |
| 2 → 3 | 5 minutes |
| 3 → 4 | 30 minutes |
| 4 → 5 | 2 hours |
| 5 → 6 | 6 hours |
A delivery fails on a non-2xx, a timeout or a connection error. After 20 consecutive failures the endpoint is disabled automatically; re-enable it in the portal once your side is fixed. The portal shows every delivery with its status and response code (not the response body).
The endpoint must be a public https URL: hostnames that resolve to private, loopback or link-local addresses are refused, and redirects are not followed — answer 2xx directly from the registered URL.
Test it
The Test button in the portal sends a signed ping event to your endpoint immediately. Verify the signature on it exactly as you would on a real event.
Rotate the secret
Delete the endpoint and create it again; the new secret is shown once. There is no in-place rotation, so create the new endpoint first, switch your verifier to accept both secrets, then delete the old one.
Reward Networks