Webhooks
Subscribe to real-time loyalty events. Every delivery is signed with HMAC-SHA256 in the X-Loyalty-Signature header — always verify before processing.
Retry schedule on failure
1 min → 5 min → 30 min → 2 hrs → final
Endpoint auto-disabled after 5 consecutive terminal failures. Re-enable via PATCH.
Endpoints
/tenants/{tenantId}/webhooksList all webhook configurations for a tenant.
/tenants/{tenantId}/webhooksRegister a new webhook endpoint with events and signing secret.
/tenants/{tenantId}/webhooks/{webhookId}Get a single webhook configuration.
/tenants/{tenantId}/webhooks/{webhookId}Update URL, rotate signing secret, or modify event subscriptions.
/tenants/{tenantId}/webhooks/{webhookId}Delete a webhook. In-flight deliveries are not affected.
/tenants/{tenantId}/webhooks/{webhookId}/testSend a test event to verify your endpoint is reachable and signature validates.
/tenants/{tenantId}/webhooks/{webhookId}/deliveriesList delivery attempts — status, response code, latency, and error body.
/tenants/{tenantId}/webhooks/{webhookId}/deliveries/{id}/replayReplay a failed delivery. Useful after fixing an endpoint outage.
Event Types
Best Practices
Always verify the signature
Use timingSafeEqual to prevent timing attacks. Reject any request where the signature does not match.
Respond 200 quickly
Return 200 immediately and process the event asynchronously. Slow responses count as failures and trigger retries.
Handle duplicates
Use event.id for deduplication — events can be replayed. Store processed IDs for at least 24 hours.
Subscribe narrowly
Only subscribe to events you need. Unused subscriptions add unnecessary load.
Register an endpoint
curl -X POST "https://loyalty-engine-production-e5cb.up.railway.app/v1/tenants/$TENANT_ID/webhooks" \
-H "X-API-Key: sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/loyalty",
"secret": "your_signing_secret_min32chars",
"events": [
"transaction.processed",
"member.enrolled",
"member.tier_changed"
]
}'Example payload
{
"id": "evt_a1b2c3d4-...",
"event": "transaction.processed",
"tenantId": "tenant-uuid",
"createdAt": "2026-05-20T14:30:00.000Z",
"data": {
"transactionId": "d2f3a1b4-...",
"transactionRef": "receipt_001",
"memberId": "9f4c2e8a-...",
"memberRef": "customer_27821234567",
"pointsAwarded": 150,
"pointsBalance": 1450,
"campaigns": [
{ "campaignId": "...", "campaignName": "Spend & Earn", "pointsEarned": 150 }
]
}
}Signature verification
// Node.js — verify X-Loyalty-Signature header
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(body, 'utf8')
.digest('hex');
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express handler
app.post('/webhooks/loyalty', express.text({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-loyalty-signature'] as string;
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
switch (event.event) {
case 'transaction.processed':
// award confirmed — update your records
break;
case 'member.tier_changed':
// notify customer of new tier
break;
}
res.status(200).json({ received: true });
});