Skip to content

Verifying signatures

Every inbound delivery to an Alfe webhook carries a signature that proves it was signed with your webhook’s secret. Alfe verifies this signature at the edge, so by the time an event reaches your agent it has already been authenticated.

You only need the details on this page if you put your own receiver in front of a webhook — for example a small service that pre-processes events before your agent sees them — and you want to verify the signature yourself. The contract below is exactly what Alfe uses.

  • Header: X-Alfe-Signature-256

  • Algorithm: HMAC-SHA256

  • Key: your webhook’s signing secret (the value returned once when you created or rotated the webhook)

  • Message: the raw, unmodified request body — the exact bytes received, before any JSON parsing or re-serialization

  • Format: the header value is the lowercase hex digest prefixed with sha256=, for example:

    X-Alfe-Signature-256: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

To verify a request:

  1. Compute HMAC-SHA256(rawBody, signingSecret) and hex-encode it.
  2. Prefix it with sha256=.
  3. Compare it to the incoming X-Alfe-Signature-256 header using a constant-time comparison.

If they match, the request is authentic. If they don’t — or the header is missing — reject it.

  • Sign the raw body, not parsed JSON. If you let a framework parse the body into an object and then re-serialize it to verify, whitespace and key ordering can change the bytes and the signature will never match. Capture the raw payload before anything touches it.
  • Compare in constant time. Use a timing-safe comparison (such as crypto.timingSafeEqual in Node) rather than ===, so an attacker can’t use response timing to guess the signature byte by byte.
import crypto from "node:crypto";
/**
* Verify an inbound Alfe webhook signature.
*
* @param rawBody The exact request body bytes (Buffer or string).
* @param secret The webhook's signing secret.
* @param signature The value of the `X-Alfe-Signature-256` header.
*/
export function verifyAlfeSignature(
rawBody: Buffer | string,
secret: string,
signature: string,
): boolean {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
// Lengths must match before timingSafeEqual, which throws on mismatch.
if (expected.length !== signature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
}

Wiring it into an Express handler — note that the raw body must be preserved:

import express from "express";
const app = express();
// Capture the raw bytes so the signature check sees exactly what was sent.
app.use(express.raw({ type: "*/*" }));
app.post("/my-receiver", (req, res) => {
const signature = req.header("X-Alfe-Signature-256") ?? "";
const secret = process.env.WEBHOOK_SIGNING_SECRET!;
if (!verifyAlfeSignature(req.body, secret, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString("utf-8"));
// …handle the verified event…
res.status(200).json({ received: true });
});

The algorithm is standard HMAC-SHA256, so any language works. In Python:

import hashlib
import hmac
def verify_alfe_signature(raw_body: bytes, secret: str, signature: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

hmac.compare_digest is Python’s constant-time comparison — the equivalent of crypto.timingSafeEqual.

When you rotate a webhook’s signing secret, deliveries are signed with the new secret from that point on. Update the secret in your receiver as part of the rotation so verification keeps passing. See Creating & managing webhooks.