> ## 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.

# Verifying webhook signatures

> Authenticate incoming webhooks using HMAC-SHA256 signatures

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:

| Header              | Description                                        | Example                                  |
| ------------------- | -------------------------------------------------- | ---------------------------------------- |
| `webhook-id`        | Unique message identifier                          | `msg_2NfDKEm9sF8xK3pQr1Zt`               |
| `webhook-timestamp` | Unix timestamp (seconds) when the message was sent | `1710510600`                             |
| `webhook-signature` | One or more HMAC-SHA256 signatures                 | `v1,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

<Steps>
  <Step title="Extract the headers">
    Get `webhook-id`, `webhook-timestamp`, and `webhook-signature` from the request headers.
  </Step>

  <Step title="Validate the timestamp">
    Reject requests where the timestamp is more than 5 minutes from your server's current time. This prevents replay attacks.
  </Step>

  <Step title="Build the signed content">
    Concatenate the webhook ID, timestamp, and raw request body with periods:

    ```
    {webhook-id}.{webhook-timestamp}.{raw_body}
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Warning>
  Always use the **raw request body** for verification. If your framework parses JSON and re-serializes it, the signature will not match.
</Warning>

## Code examples

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    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);
        }
      }
    );
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib
    import base64
    import time
    import json

    def verify_webhook(payload: bytes, headers: dict, secret: str) -> dict:
        webhook_id = headers.get("webhook-id")
        webhook_timestamp = headers.get("webhook-timestamp")
        webhook_signature = headers.get("webhook-signature")

        # Validate timestamp (5-minute tolerance)
        now = int(time.time())
        if abs(now - int(webhook_timestamp)) > 300:
            raise ValueError("Timestamp too old")

        # Build signed content
        signed_content = f"{webhook_id}.{webhook_timestamp}.{payload.decode()}"

        # Decode secret (strip "whsec_" prefix, base64-decode)
        secret_bytes = base64.b64decode(secret.split("_")[1])

        # Compute HMAC-SHA256
        computed = base64.b64encode(
            hmac.new(
                secret_bytes,
                signed_content.encode(),
                hashlib.sha256,
            ).digest()
        ).decode()

        # Compare against each signature in the header
        signatures = [
            sig.split(",")[1]
            for sig in webhook_signature.split(" ")
        ]

        if computed not in signatures:
            raise ValueError("Invalid signature")

        return json.loads(payload)

    # Flask example
    from flask import Flask, request

    app = Flask(__name__)

    @app.route("/webhooks/allo", methods=["POST"])
    def handle_webhook():
        try:
            event = verify_webhook(
                request.get_data(),
                request.headers,
                os.environ["WEBHOOK_SECRET"],
            )
            print(f"Verified event: {event['topic']}")
            return "", 200
        except ValueError as e:
            print(f"Verification failed: {e}")
            return "", 400
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    package main

    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/base64"
        "fmt"
        "math"
        "net/http"
        "strconv"
        "strings"
        "time"
    )

    func verifyWebhook(payload []byte, headers http.Header, secret string) error {
        webhookID := headers.Get("webhook-id")
        webhookTimestamp := headers.Get("webhook-timestamp")
        webhookSignature := headers.Get("webhook-signature")

        // Validate timestamp (5-minute tolerance)
        ts, _ := strconv.ParseInt(webhookTimestamp, 10, 64)
        if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
            return fmt.Errorf("timestamp too old")
        }

        // Build signed content
        signedContent := fmt.Sprintf("%s.%s.%s", webhookID, webhookTimestamp, string(payload))

        // Decode secret (strip "whsec_" prefix, base64-decode)
        parts := strings.SplitN(secret, "_", 2)
        secretBytes, _ := base64.StdEncoding.DecodeString(parts[1])

        // Compute HMAC-SHA256
        mac := hmac.New(sha256.New, secretBytes)
        mac.Write([]byte(signedContent))
        computed := base64.StdEncoding.EncodeToString(mac.Sum(nil))

        // Compare against each signature in the header
        for _, sig := range strings.Split(webhookSignature, " ") {
            parts := strings.SplitN(sig, ",", 2)
            if len(parts) == 2 && parts[1] == computed {
                return nil
            }
        }

        return fmt.Errorf("invalid signature")
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Always verify signatures in production. Skipping verification exposes your application to spoofed webhook events.
</Warning>
