Webhook Signing
Every backend webhook Akedly sends to your backendCallbackURL is HMAC-SHA256 signed with a per-pipeline secret you control. Verify the signature before trusting the payload — it's the only way to be sure the request actually came from Akedly and wasn't crafted by anyone who guessed your URL.
We follow the Standard Webhooks specification (the same scheme Svix uses), so any official Svix SDK works out of the box.
Why webhooks are signed
Your backendCallbackURL lives on the public internet. Anyone who learns the URL — through logs, browser network tabs, mistyped DNS, a leaked screenshot — can POST forged payloads to it claiming a verification succeeded. Without a signature check, your application has no way to tell a real Akedly webhook from a fake one.
Signing solves this. Akedly hashes the request body together with a timestamp and a unique message ID, using a secret only you and Akedly know. You re-compute the hash on your side; if it matches, the request is genuine and untampered.
Where to find your secret
Each pipeline has its own signing secret, prefixed with whsec_. To retrieve it:
- Open your pipeline in the dashboard
- Scroll to the Callbacks card
- Click View signing secret
View-once
The secret is shown one time only. Copy it into your secret manager immediately — once you click I've saved it, the dashboard will refuse to show it again. If you lose it, click Regenerate to mint a new one (this invalidates the old secret).
The secret looks like this:
Example secret
whsec_kU3+tF2dG8XzL9pQrSvT4Bh6jYwMnRb1cZxK7eNoAi0=
Treat it like a password. Store it in your environment, never commit it, and rotate it if you suspect exposure.
The signed headers
Every signed webhook arrives with three headers:
- Name
svix-id- Type
- string
- Description
Unique message ID, like
msg_2N4kJp.... Use it to deduplicate retries.
- Name
svix-timestamp- Type
- string
- Description
Unix timestamp (seconds) when the signature was generated. Reject any request where the timestamp drifts more than ~5 minutes from your server clock to prevent replay attacks.
- Name
svix-signature- Type
- string
- Description
Space-separated list of one or more signatures, each prefixed with a version (
v1,sig). During key rotation you may receive multiple — accept the request if any one matches.
Example headers
svix-id: msg_2N4kJpQrSvT4Bh6jYwMnRb1cZx
svix-timestamp: 1745190600
svix-signature: v1,kU3+tF2dG8XzL9pQrSvT4Bh6jYwMnRb1cZxK7eNoAi0=
Signing algorithm
If you're using one of the official Svix SDKs (next section), you don't need to implement this — it's here for reference and for any stack without an SDK.
Algorithm
1. Decode the secret:
key = base64_decode( secret.replace("whsec_", "") )
2. Build the signed payload:
signed = "${svix-id}.${svix-timestamp}.${raw_body}"
3. Compute the expected signature:
expected = base64_encode( HMAC_SHA256(key, signed) )
4. Compare against each signature in the svix-signature header
(constant-time compare). If any match, the request is valid.
5. Reject if | svix-timestamp − now | > 5 minutes (replay protection).
The raw_body is the unparsed request body bytes — don't JSON.parse then re-serialize, or you'll lose the exact byte sequence the signature was computed over.
Verify in your stack
Pick your language. The signing algorithm is identical across all of them — these snippets just wrap the math.
Verify webhook signature
// npm install svix
const { Webhook } = require('svix')
const wh = new Webhook(process.env.AKEDLY_WEBHOOK_SECRET)
// rawBody must be the unparsed string body — not req.body (parsed JSON).
// In Express, mount express.raw({ type: 'application/json' }) on this route.
wh.verify(rawBody, req.headers) // throws WebhookVerificationError if invalid
Common pitfalls
Three things that catch most integrators
1. Forgetting to base64-decode the secret. The whsec_ prefix isn't decorative — strip it, then base64-decode the rest. The decoded bytes are the actual HMAC key. Hashing with the prefixed string directly will never validate.
2. Hashing the raw body alone. The signed string is "${id}.${timestamp}.${body}", not body. Skip the prefix and your signature won't match.
3. Reading req.body after framework parsing. Express's default JSON parser reads the stream and exposes parsed JSON on req.body — the raw bytes are gone. Mount express.raw({ type: 'application/json' }) on your webhook route (or its equivalent in your framework) so the unparsed bytes are still available for verification.
Rotating the secret
If a secret leaks — or just on a routine schedule — click Regenerate in the Callbacks card. Akedly issues a new whsec_... and immediately starts signing webhooks with it.
Plan for the cutover
There's no overlap window. The moment you regenerate, every subsequent webhook is signed with the new secret. Update your environment with the new value and redeploy before the next webhook fires, or your verifier will reject good payloads.
If you can't deploy instantly, deploy a temporary dual-secret check (try old, then new) ahead of the rotation, then drop the old one once traffic settles.