Security & HMAC
Every webhook request includes a signature header so you can verify the request came from Loyalite and wasn't tampered with in transit.
The signature header
X-Loyalite-Signature-256: sha256=<hex>The signature is an HMAC-SHA256 of the raw request body, using your endpoint's signing secret as the key. The format follows the same convention as GitHub webhooks (opens in a new tab).
Verification steps
- Read the raw request body before parsing JSON.
- Compute
HMAC-SHA256(secret, raw_body)and hex-encode the result. - Prepend
sha256=to get the expected signature. - Compare with the
X-Loyalite-Signature-256header using a timing-safe comparison. - If they match, the request is authentic — proceed to parse and handle it.
- If they don't match, return
401or403immediately.
Always use a constant-time comparison (e.g. hmac.Equal, secrets.compare_digest, hash_equals). String equality operators are vulnerable to timing attacks (opens in a new tab).
Code samples
import crypto from 'crypto';
// Express middleware example
function verifyLoyaliteSignature(secret) {
return (req, res, next) => {
const signature = req.headers['x-loyalite-signature-256'];
if (!signature) {
return res.status(401).json({ error: 'Missing signature' });
}
// Use raw body — configure express to expose it:
// app.use(express.json({ verify: (req, _, buf) => { req.rawBody = buf; } }))
const hmac = crypto.createHmac('sha256', secret);
hmac.update(req.rawBody);
const expected = 'sha256=' + hmac.digest('hex');
const valid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!valid) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
// Usage
app.post('/webhooks/loyalite', verifyLoyaliteSignature(process.env.WEBHOOK_SECRET), (req, res) => {
const { event, data } = req.body;
console.log('Received event:', event, data);
res.sendStatus(200);
});Rotating your secret
If your secret is ever compromised, use Rotate Secret in the merchant app (Settings → Integrations → Webhooks → tap endpoint → Rotate Secret).
Rotating your secret immediately invalidates the old one. Update your server's WEBHOOK_SECRET environment variable before rotating, or rotate during a maintenance window to avoid a gap in signature verification.
The new secret is displayed once after rotation — same rules as initial creation.
Replay attack prevention
Loyalite does not currently include a timestamp in the signature. To prevent replay attacks in high-security environments, track processed delivery IDs (the id field in the payload) and reject duplicates:
const processedIds = new Set(); // use Redis or a DB in production
app.post('/webhooks/loyalite', verifyLoyaliteSignature(secret), (req, res) => {
const { id, event, data } = req.body;
if (processedIds.has(id)) {
return res.sendStatus(200); // already processed — idempotent response
}
processedIds.add(id);
// handle event...
res.sendStatus(200);
});