POST to your endpoint includes the X-Keebai-Signature header. If you don’t verify it, anyone who knows your endpoint URL can send fake payloads. Always verify.
Anatomy of the signature
The signed payload is the timestamp and the raw body concatenated with a dot, not the body alone. That stops anyone from taking an old valid body and resending it with a different timestamp.
Verification steps
1
Capture the raw body
Before parsing JSON. If your framework parses automatically, configure it to keep the raw bytes too (in Express, use
bodyParser.raw for this route or read req.rawBody).2
Parse the header
Pull
t and v1 out of the X-Keebai-Signature header.3
Rebuild the signed payload
signedPayload = ${t}.${rawBody}.4
Compute HMAC-SHA256 with your secret
expected = HMAC-SHA256(secret, signedPayload).toString('hex').5
Constant-time comparison
Compare
expected to v1 using a constant-time comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). Don’t use plain ===: it’s vulnerable to timing attacks.6
Anti-replay (optional but recommended)
Reject if
now - t > 5 minutes. An attacker could capture an old delivery and replay it if your endpoint is public.Examples
Best practices
Raw body, not parsed
The HMAC is computed over the exact bytes. If your framework parses and reserializes the JSON, whitespace shifts and the signature stops matching.
Constant-time comparison
=== and == leak length information and enable timing attacks. Always use timingSafeEqual / hmac.compare_digest / hash_equals.Verify BEFORE any side-effect
Don’t process the event, don’t log details, don’t write to your DB until the signature is valid. Return 401 immediately on failure.
Dedup by `event.id`
We retry on 5xx / timeout / 408 / 429. Your endpoint may receive the same
id multiple times. Keep a table of processed event_id values (7-day TTL).Secret rotation
POST /v1/webhooks/:id/rotate-secret (or keebai webhooks rotate <id>) generates a new secret. The old one is invalidated immediately — there is no grace period. Recommended rotation strategy:
1
Configure your endpoint to accept 2 secrets at once
One variable
KEEBAI_WEBHOOK_SECRET (current) and another KEEBAI_WEBHOOK_SECRET_NEXT (staged). The handler tries the first one; if it fails, it tries the second.2
Rotate
keebai webhooks rotate <id>. Paste the new secret into KEEBAI_WEBHOOK_SECRET_NEXT and deploy.3
Promote
Once you’ve confirmed the new one works, move it to
KEEBAI_WEBHOOK_SECRET and remove the old one.