Akedly

Webhooks

A webhook is how Akedly tells your server that something happened — a message was delivered, a customer replied, a verification finished. You give us a URL, and we send a message to it.


What is a webhook?

Normally you call us: you send a request, we send a response. A webhook is the other direction — something happens on our side, and we call you.

The alternative is polling: asking us "has it delivered yet?" every few seconds. That is slow and wasteful. With a webhook you find out the moment it happens.

In practice a webhook is just an HTTP POST to a URL you own. If you can handle a form submission, you can handle a webhook:

A minimal endpoint

app.post('/webhooks/akedly', (req, res) => {
  res.sendStatus(200)   // 1. say "got it" immediately
  handle(req.body)      // 2. then do your work
})

What Akedly can send you

Authentication has two webhook paths and they do not behave the same way. Which applies depends on how you integrate.

You are usingYou getSigned?Retried?History?
V2 WidgetThe verification resultUsuallyNoNo
OTP API (V1.0 / V1.2)The verification resultNoNoNo

Both send once. There is no retry and no delivery record on either, so neither should be your only record of a verification — confirm anything that matters from the API. For idempotency, use a stable identifier from the payload: transactionID in the V1/V1.2 mainTransaction object, or widgetAttempt.attemptId for V2.


Setting up your endpoint

Your webhook URL lives on the pipeline, in its Webhooks section. Paste a URL, save, and we start sending. Leave it empty and we send nothing.

The same section holds your signing secret — the value you need to prove a message came from us.


Try it before you have traffic

The dashboard can fire a sample request at your URL and show you exactly what your server replied — useful long before any real traffic exists.

Open Test your endpoint in the Webhooks section, check the headers and body we will send, press Send, and the response panel shows your server's live answer.


Why webhooks are signed

Your callback URL lives on the public internet. Anyone who learns it — 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 Webhooks section
  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:

Example headers

svix-id:        msg_2N4kJpQrSvT4Bh6jYwMnRb1cZx
svix-timestamp: 1745190600
svix-signature: v1,kU3+tF2dG8XzL9pQrSvT4Bh6jYwMnRb1cZxK7eNoAi0=

The svix-signature value contains one or more space-separated entries, each in the form v1,<base64>.

Webhook contract

  • Name
    svix-id
    Type
    header
    Description
    A fresh msg_* identifier is generated for each dispatcher call. Utility retries therefore receive a new value on each attempt.
  • Name
    svix-timestamp
    Type
    Unix seconds
    Description
    The signing time converted to a Unix timestamp in seconds.
  • Name
    svix-signature
    Type
    header
    Description
    The signature returned by Svix Webhook.sign over the message ID, timestamp, and serialized payload.
  • Name
    Signed body
    Type
    JSON string
    Description
    Akedly signs JSON.stringify(payload) and sends that same string as the POST body.
  • Name
    Generated-header precedence
    Type
    server-controlled
    Description
    Customer-supplied svix-*, content-type, user-agent, host, and content-length headers are filtered before Akedly adds its generated headers.
  • Name
    HTTP timeout
    Type
    10 seconds
    Description
    Each common webhook POST uses a 10-second Axios timeout.
  • Name
    V2 Widget signing
    Type
    signed
    Description
    The widget callback path passes the pipeline webhook signing secret to the common dispatcher.
  • Name
    V2 Widget retries
    Type
    single dispatcher call
    Description
    The V2 callback path has no application-level retry loop; retry behavior outside this path is not defined by this contract.
  • Name
    Utility signing
    Type
    required
    Description
    Utility delivery calls the common dispatcher with signature required.
  • Name
    Utility retries
    Type
    up to 5 attempts
    Description
    A utility delivery retries failed dispatches up to five times.
  • Name
    Retry backoff
    Type
    1s, 5s, 25s, 125s
    Description
    The default delays between utility attempts are one, five, 25, and 125 seconds.
  • Name
    Dedupe key
    Type
    unique for 7 days
    Description
    Dispatch admission claims dedupeKey against a unique index with a seven-day TTL. Status triggers use message.status:<channel>:<pipeline>:<message-or-transaction>:<status>; inbound and button triggers use <eventType>:<pipeline>:<wamid>.
  • Name
    Stable event ID
    Type
    eventId
    Description
    The envelope creates one eventId that is reused across retries; the per-attempt svix-id is not the stable deduplication identifier.
  • Name
    Delivery history
    Type
    owner-scoped API
    Description
    Utility delivery records are available at GET /api/v1/utilities/pipelines/:id/webhook-deliveries, newest first, with page and limit values clamped to 1–100 and a default limit of 20.
Event typeData fields
message.statustransactionId, wamid, smsMessageId, templateId, phoneNumber, status, failureCode, failureReason, channel, statusTimestamp
message.inboundwamid, contextWamid, transactionId, templateId, phoneNumber, messageBody, receivedAt, sourceKnown
button.actionwamid, contextWamid, transactionId, templateId, phoneNumber, buttonId, buttonLabel, buttonPayload, tappedAt, sourceKnown

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. Split svix-signature on spaces. For each entry, strip the `v1,` prefix before the
   constant-time compare with the computed base64 value. 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 pipeline's Webhooks section. Akedly issues a new whsec_... and immediately starts signing webhooks with it.

Was this page helpful?