Webhooks
Verify a signature
Webhooks are signed with your secret, over the timestamp and the raw body. Check it before you trust the body, and drop an event you have already handled.
#Four checks
- Read
X-Laayo-Timestamp. Refuse it if it is more than 5 minutes from your clock. - Compute the lowercase hex HMAC-SHA256 of
timestamp + '.' + raw bodywith your secret. There is no method or path in it: you own that URL. - Compare it with
X-Laayo-Signaturein constant time. Refuse with 401 if it differs; we retry. - Skip the event if you have already handled its
X-Laayo-Idempotency-Key, but still answer 2xx.
#Code
const crypto = require('crypto');
app.post('/laayo/webhook', express.raw({ type: '*/*' }), (req, res) => {
const ts = req.get('X-Laayo-Timestamp') || '';
const given = req.get('X-Laayo-Signature') || '';
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const expected = crypto.createHmac('sha256', process.env.LAAYO_SECRET)
.update(ts + '.' + req.body).digest('hex');
if (expected.length !== given.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
return res.sendStatus(401);
}
const key = req.get('X-Laayo-Idempotency-Key');
if (alreadyHandled(key)) return res.sendStatus(200);
queue.push(JSON.parse(req.body)); // handle after answering
res.sendStatus(200);
});#Use the raw body
Most frameworks parse JSON before your handler runs. Parsing and encoding again changes spacing and key order, and the signature stops matching. Read the raw bytes for the check, then parse.
# what Laayo signs for one delivery
1762003860.{"event":"test","idempotency_key":"test:7:1762003860","occurred_at":"2026-11-01T12:51:00+05:45","data":{"message":"Test event from Laayo. Answer 2xx to acknowledge.","partner_id":7}}#After a rotation
Webhooks are always signed with your current secret. When you rotate with POST /v1/partner/secret/rotate, your calls may use the old secret for 24 hours, but deliveries switch to the new one at once. Update your webhook handler first, or check against both while you switch. A delivery you refuse meanwhile is retried, so nothing is lost.
Your calls to Laayo are signed differently, with the method and path in the base. See Signing requests.
Was this page clear?Updated 1 Nov 2026