Webhooks
Subscribe to events, verify signatures, and test your handler.
Webhooks push events to your server as rewards move through their lifecycle — no polling required.
Subscribe
curl -X POST https://paybilt-rewards-api.fly.dev/v1/webhook-endpoints \
-H "Authorization: Bearer $REWARDLY_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://api.yourapp.com/rewardly-webhook", "events": ["reward.redeemed", "card.issued"]}'An empty events array subscribes to everything. The response includes the
endpoint's signing secret (whsec_...) — store it now, it's shown
once. Rotate it later with
POST /v1/webhook-endpoints/:id/rotate-secret.
Event catalog
| Event | Fires when |
|---|---|
reward.created | A reward is accepted and funds are held |
reward.sent | The recipient email is queued |
reward.opened | The recipient visits their redemption link |
reward.partially_redeemed | A choice reward has some value used |
reward.redeemed | The full value has been turned into cards |
reward.expired | A reward hit its expiry; remaining value returned |
reward.canceled | You canceled a reward |
card.issued | A gift card was successfully issued |
card.failed | A card could not be issued (value re-credited) |
wallet.credited | A top-up settled |
Payload shape
{
"id": "evt_a1b2c3d4e5f6",
"type": "reward.redeemed",
"createdAt": "2026-08-06T12:00:00.000Z",
"data": { "id": "rw_...", "status": "REDEEMED", "valueCents": 5000 }
}Verify signatures
Every delivery is signed. Compute an HMAC-SHA256 of
"{X-Webhook-Timestamp}.{raw request body}" with your endpoint secret and
compare it to the X-Webhook-Signature header (v1=<hex>):
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(req: { headers: Record<string, string>; rawBody: string }) {
const timestamp = req.headers["x-webhook-timestamp"];
const signature = req.headers["x-webhook-signature"]; // "v1=abc123..."
// reject replays older than 5 minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = `v1=${createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(`${timestamp}.${req.rawBody}`)
.digest("hex")}`;
return (
expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
);
}Use the raw request body — parsing and re-serializing JSON will change the bytes and break the signature.
Test your handler
Queue a signed ping event any time:
curl -X POST https://paybilt-rewards-api.fly.dev/v1/webhook-endpoints/we_123/test \
-H "Authorization: Bearer $REWARDLY_KEY"Then check the result with
GET /v1/webhook-endpoints/:id/deliveries,
which shows status, attempt count, and the HTTP status your server returned.
Delivery behavior
- Respond with any
2xxquickly (do the work async) — anything else counts as a failure and is retried with backoff. - Deliveries can arrive out of order; use
createdAtor reward status rather than arrival order. - Handlers should be idempotent: use the event
idto dedupe. - Pause an endpoint without deleting it via
PATCH /v1/webhook-endpoints/:idwith{"active": false}.