Skip to main content

Documentation Index

Fetch the complete documentation index at: https://help.withallo.com/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook request includes a cryptographic signature so you can verify it came from Allo. Without verification, an attacker could send fake events to your endpoint.

Signature headers

Each webhook POST includes three headers:
HeaderDescriptionExample
webhook-idUnique message identifiermsg_2NfDKEm9sF8xK3pQr1Zt
webhook-timestampUnix timestamp (seconds) when the message was sent1710510600
webhook-signatureOne or more HMAC-SHA256 signaturesv1,K5oZfzN95Z3M+nrYOgGHfLGC7t8VjLtV...

Signing secret

When you create a webhook endpoint, the response includes a signing_secret field. This secret has the format whsec_<base64key> and is used to verify signatures. Store it securely — treat it like a password. If compromised, rotate it via the API.

Verification algorithm

1

Extract the headers

Get webhook-id, webhook-timestamp, and webhook-signature from the request headers.
2

Validate the timestamp

Reject requests where the timestamp is more than 5 minutes from your server’s current time. This prevents replay attacks.
3

Build the signed content

Concatenate the webhook ID, timestamp, and raw request body with periods:
{webhook-id}.{webhook-timestamp}.{raw_body}
4

Compute the expected signature

  1. Strip the whsec_ prefix from your signing secret.
  2. Base64-decode the remaining string to get the key bytes.
  3. Compute HMAC-SHA256 of the signed content using the key bytes.
  4. Base64-encode the result.
5

Compare signatures

The webhook-signature header may contain multiple signatures separated by spaces (for secret rotation). Each has a v1, prefix. Compare your computed signature against each one. If any match, the signature is valid.
Always use the raw request body for verification. If your framework parses JSON and re-serializes it, the signature will not match.

Code examples

const crypto = require("crypto");

function verifyWebhook(payload, headers, secret) {
  const webhookId = headers["webhook-id"];
  const webhookTimestamp = headers["webhook-timestamp"];
  const webhookSignature = headers["webhook-signature"];

  // Validate timestamp (5-minute tolerance)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(webhookTimestamp)) > 300) {
    throw new Error("Timestamp too old");
  }

  // Build signed content
  const signedContent = `${webhookId}.${webhookTimestamp}.${payload}`;

  // Decode secret (strip "whsec_" prefix, base64-decode)
  const secretBytes = Buffer.from(secret.split("_")[1], "base64");

  // Compute HMAC-SHA256
  const computed = crypto
    .createHmac("sha256", secretBytes)
    .update(signedContent)
    .digest("base64");

  // Compare against each signature in the header
  const signatures = webhookSignature
    .split(" ")
    .map((sig) => sig.split(",")[1]);

  if (!signatures.some((sig) => sig === computed)) {
    throw new Error("Invalid signature");
  }

  return JSON.parse(payload);
}

// Express.js example
const express = require("express");
const app = express();

app.post(
  "/webhooks/allo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = verifyWebhook(
        req.body.toString(),
        req.headers,
        process.env.WEBHOOK_SECRET
      );
      console.log("Verified event:", event.topic);
      res.sendStatus(200);
    } catch (err) {
      console.error("Verification failed:", err.message);
      res.sendStatus(400);
    }
  }
);
Always verify signatures in production. Skipping verification exposes your application to spoofed webhook events.