Webhooks
Receive real-time events from your loyalty program
Webhooks push events to your server the moment they happen — no polling required. Register an HTTPS endpoint and choose which event types to receive.
Webhook Events
| Event | Fired when |
|---|---|
member.created | A new member is created |
member.updated | A member's profile is updated |
points.awarded | Points are awarded to a member |
points.deducted | Points are deducted from a member |
badge.awarded | A badge is earned |
quest.completed | A quest is completed |
referral.converted | A referral is converted |
promo.redeemed | A promo code is redeemed |
reward.redeemed | A reward is redeemed |
List Webhooks
GET /api/v1/webhooksSDK
const webhooks = await useLoyalty.webhooks.list();Register a Webhook
POST /api/v1/webhooksRequest Body
{
"url": "https://yourapp.com/webhooks/useloyalty",
"events": ["points.awarded", "quest.completed", "reward.redeemed"]
}SDK
const webhook = await useLoyalty.webhooks.create({
url: 'https://yourapp.com/webhooks/useloyalty',
events: ['points.awarded', 'quest.completed', 'reward.redeemed'],
});Response
{
"id": "wh_abc123",
"url": "https://yourapp.com/webhooks/useloyalty",
"events": ["points.awarded", "quest.completed", "reward.redeemed"],
"isActive": true,
"createdAt": "2024-01-20T10:00:00Z"
}Delete a Webhook
DELETE /api/v1/webhooks/:webhookIdSDK
await useLoyalty.webhooks.delete('wh_abc123');Test a Webhook
Send a test payload to your endpoint to verify it's receiving correctly.
POST /api/v1/webhooks/:webhookId/testSDK
const { success } = await useLoyalty.webhooks.test('wh_abc123');Verifying Webhook Signatures
Every webhook request includes an X-UseLoyalty-Signature header. Always verify it before processing — it proves the request came from useLoyalty, not a third party.
import { verifyWebhookSignature } from '@useloyalty/sdk';
// Express — use raw body middleware BEFORE json parser for this route
app.post(
'/webhooks/useloyalty',
express.raw({ type: 'application/json' }),
(req, res) => {
const isValid = verifyWebhookSignature(
req.body.toString(), // raw string body
req.headers['x-useloyalty-signature'] as string,
process.env.USELOYALTY_WEBHOOK_SECRET!
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
handleEvent(event);
res.send('ok');
}
);Pass the raw request body string to verifyWebhookSignature — do not parse it as JSON first. Parsing and re-serializing changes whitespace and key order, which breaks the signature check.
Next.js App Router
// app/api/webhooks/useloyalty/route.ts
import { verifyWebhookSignature } from '@useloyalty/sdk';
export async function POST(req: Request) {
const body = await req.text(); // raw string
const signature = req.headers.get('x-useloyalty-signature') ?? '';
const isValid = verifyWebhookSignature(
body,
signature,
process.env.USELOYALTY_WEBHOOK_SECRET!
);
if (!isValid) {
return new Response('Unauthorized', { status: 401 });
}
const event = JSON.parse(body);
await handleUseLoyaltyEvent(event);
return new Response('ok');
}Webhook Payload Shape
{
"id": "evt_xyz789",
"type": "points.awarded",
"timestamp": "2024-01-20T10:30:00Z",
"signature": "sha256=abc123...",
"data": {
"memberId": "clx123abc",
"externalId": "user_123",
"pointsAwarded": 100,
"newBalance": 1600,
"transactionId": "txn_abc"
}
}Handling Events
async function handleUseLoyaltyEvent(event: WebhookEvent) {
switch (event.type) {
case 'points.awarded':
await notifyUser(event.data.externalId, `You earned ${event.data.pointsAwarded} points!`);
break;
case 'quest.completed':
await notifyUser(event.data.externalId, `Quest complete! You earned ${event.data.pointsAwarded} points.`);
break;
case 'badge.awarded':
await notifyUser(event.data.externalId, `New badge: ${event.data.badgeName}!`);
break;
case 'reward.redeemed':
if (event.data.couponCode) {
await sendCouponEmail(event.data.externalId, event.data.couponCode);
}
break;
}
}Retry Behavior
useLoyalty retries failed webhook deliveries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
A delivery is considered failed if your endpoint returns a non-2xx status or doesn't respond within 10 seconds. After 5 failed attempts the webhook is marked as failed and no further retries occur.
Return a 200 response immediately and process the event asynchronously (e.g. push to a queue). Long-running handlers increase the chance of timeouts and duplicate deliveries.