Webhook Signatures
Every outbound webhook from ScaiVault is signed with HMAC-SHA256. Your endpoint must verify the signature before trusting the payload. Without verification, anyone who knows your URL can forge events.
Headers on delivery#
1 2 3 4 5 | |
Timestamp— Unix seconds at dispatch.Signature—sha256=followed by hex HMAC-SHA256.
What gets signed#
HMAC(secret, "{timestamp}.{raw_body}") — the timestamp, a literal ., and the raw request body bytes. No JSON reparsing, no header normalization.
Verify in Python#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
Verify in TypeScript / Node#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Two important details for JavaScript:
- Use the raw request body, not a re-serialized JSON object. Body parsing rearranges whitespace and breaks the signature.
- Use
timingSafeEqualto prevent timing attacks.
Verify in curl (for debugging)#
1 2 3 4 | |
Compare against what arrived in X-ScaiVault-Signature.
Replay protection#
The X-ScaiVault-Timestamp header plus a max-age check (5 minutes is standard) prevents replay of captured requests. A captured request is only valid for 5 minutes after the dispatch time.
For stronger replay protection, persist X-ScaiVault-Event-Id in a short-TTL store and reject duplicates:
1 2 3 | |
Secret rotation#
Each webhook has a secret configured at registration. To rotate:
-
Generate a new secret.
-
PATCH the webhook with the new secret and add the old one as
previous_secret(kept for 24h):bash1 2 3 4
curl -X PATCH https://scaivault.scailabs.ai/v1/webhooks/wh_abc \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"secret": "new-secret-here", "previous_secret_ttl": "24h"}' -
During the overlap, ScaiVault signs with the new secret; your endpoint accepts either the new or old signature.
-
After the TTL passes, only the new secret is valid.
Your verification code for the overlap:
1 2 3 4 5 6 | |
Common mistakes#
- Parsing JSON before verifying. Many frameworks re-serialize the body after parsing. You must sign and verify the raw bytes.
- No timestamp check. Without one, a captured webhook is replayable forever.
- String comparison for signatures. Use a constant-time compare (
hmac.compare_digestin Python,timingSafeEqualin Node). - Leaking signature details. Don't respond with "bad signature expected X got Y" — that helps an attacker. A plain
401is fine.
What's next#
- Events and Webhooks — event catalog and delivery semantics.
- Webhooks Reference — management endpoints.