Webhook signatures
Every request to an Alfe webhook must carry a signature made with that webhook’s secret.
Alfe checks it before anything reaches your agent, and it rejects a missing or invalid
signature with a 401.
The signature contract
Section titled “The signature contract”Alfe expects this:
- Header —
X-Alfe-Signature-256. - Algorithm — HMAC-SHA256.
- Key — the webhook’s signing secret, exactly as Alfe returned it.
- Message — the raw request body, as the exact bytes you send.
- Format —
sha256=followed by the lowercase hex digest.
X-Alfe-Signature-256: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08Sign the exact bytes you send. If you sign an object and then serialize it again, whitespace or key order can change, and the signature won’t match.
Send a signed request
Section titled “Send a signed request”These examples sign a JSON body and send it to a webhook.
BODY='{"event":"deploy.finished","service":"api"}'SIGNATURE="sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex | awk '{print $NF}')"
curl "https://webhooks.alfe.ai/v1/hooks/$WEBHOOK_ID" \ -H "Content-Type: application/json" \ -H "X-Alfe-Signature-256: $SIGNATURE" \ -d "$BODY"import crypto from "node:crypto";
const body = JSON.stringify({ event: "deploy.finished", service: "api" });const signature = "sha256=" + crypto .createHmac("sha256", process.env.SIGNING_SECRET!) .update(body) .digest("hex");
await fetch(process.env.WEBHOOK_URL!, { method: "POST", headers: { "Content-Type": "application/json", "X-Alfe-Signature-256": signature, }, body,});import hashlibimport hmacimport jsonimport osimport urllib.request
body = json.dumps({"event": "deploy.finished", "service": "api"}).encode()signature = "sha256=" + hmac.new( os.environ["SIGNING_SECRET"].encode(), body, hashlib.sha256).hexdigest()
request = urllib.request.Request( os.environ["WEBHOOK_URL"], data=body, headers={ "Content-Type": "application/json", "X-Alfe-Signature-256": signature, },)urllib.request.urlopen(request)Receive events from GitHub, Stripe, or Slack
Section titled “Receive events from GitHub, Stripe, or Slack”These services sign with their own headers, and you can’t change that. So they can’t call an Alfe webhook URL directly. Put a small relay between them:
- Point the service’s webhook at your relay.
- In the relay, verify the service’s own signature, using its documented scheme.
- Sign the same raw body with your Alfe secret.
- Forward the body to the webhook URL, and pass along any delivery ID header, so Alfe can spot duplicates.
import crypto from "node:crypto";import express from "express";
const app = express();
// Keep the raw bytes, so both signatures cover exactly what was sent.app.use(express.raw({ type: "*/*" }));
app.post("/github", async (req, res) => { // Verify GitHub's X-Hub-Signature-256 here, and return 401 if it fails.
// Re-sign the same bytes for Alfe. const signature = "sha256=" + crypto .createHmac("sha256", process.env.ALFE_SIGNING_SECRET!) .update(req.body) .digest("hex");
const response = await fetch(process.env.ALFE_WEBHOOK_URL!, { method: "POST", headers: { "Content-Type": req.header("content-type") ?? "application/json", "X-Alfe-Signature-256": signature, "X-GitHub-Delivery": req.header("x-github-delivery") ?? "", "X-GitHub-Event": req.header("x-github-event") ?? "", }, body: req.body, });
res.sendStatus(response.ok ? 200 : 502);});Alfe passes Content-Type, X-GitHub-Event, X-GitHub-Delivery, and Stripe-Signature
through to your agent with the payload. It drops other headers.
Rotate a secret
Section titled “Rotate a secret”When you rotate a webhook’s secret, the old one stops working right away. Update your sender as part of the rotation. See Rotate the signing secret.
Next steps
Section titled “Next steps”- Delivery and retries: see the responses your sender gets.
- Create and manage webhooks: get a URL and secret.