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:

  1. Open your pipeline in the dashboard
  2. Scroll to the Callbacks card
  3. Click View signing 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


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.

Was this page helpful?