Webhooks
#Subscribing
curl -X POST "https://api.agentflowbind.com/v1/webhooks" \
-H "Authorization: Bearer afb_live_xxxxxxxxxxxx" -H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/afb",
"events": ["run.succeeded", "run.failed", "run.needs_approval"],
"secret": "whsec_generate_a_long_random_string"
}'Valid events values: run.succeeded, run.failed, run.needs_input, run.needs_approval, run.needs_review — one entry per run status you care about; a run's other status transitions (queued → running) never fire a webhook. url must resolve to a public address — a private/loopback/link-local target is rejected at registration time (and re-checked on every delivery attempt) unless AFB_ALLOW_PRIVATE_WEBHOOKS is set for local development.
curl "https://api.agentflowbind.com/v1/webhooks" -H "Authorization: Bearer afb_live_xxxxxxxxxxxx"
curl -X DELETE "https://api.agentflowbind.com/v1/webhooks/{id}" -H "Authorization: Bearer afb_live_xxxxxxxxxxxx"The secret you provide is never returned by GET/POST — only { id, url, events, status, createdAt, updatedAt } come back.
#What gets delivered
Each matching event is a POST with this JSON body:
{
"id": "3a1e9c40-...",
"type": "run.succeeded",
"run": { "id": "5e6a7b8c-...-9d0e", "status": "succeeded", "output": { "...": "..." }, "error": null }
}signed with:
X-AFB-Signature: t=1771059123,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt is a Unix timestamp (seconds); v1 is HMAC-SHA256(secret, "${t}.${rawBody}") in hex, using the webhook's own secret. A failed delivery (endpoint returns a non-2xx or a redirect, or the request errors) is retried up to 3 times total with exponential backoff (1s, 2s, ...); a delivery is considered failed after 3 attempts.
#Verifying a delivery
Always verify the signature before trusting a payload — anyone who knows (or guesses) your endpoint URL can POST to it otherwise. Use a timing-safe comparison and reject old timestamps to prevent replay:
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_SIGNATURE_AGE_SECONDS = 5 * 60;
export function verifyWebhookSignature(secret: string, header: string, rawBody: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=") as [string, string]),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - t) > MAX_SIGNATURE_AGE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
const givenBuf = Buffer.from(v1, "hex");
return expectedBuf.length === givenBuf.length && timingSafeEqual(expectedBuf, givenBuf);
}// A framework-agnostic handler, e.g. behind an Express/Hono/Next.js route:
app.post("/hooks/afb", async (req) => {
const rawBody = await req.text(); // verify against the *raw* body, before any JSON.parse
const signature = req.headers.get("x-afb-signature") ?? "";
if (!verifyWebhookSignature(process.env.AFB_WEBHOOK_SECRET!, signature, rawBody)) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody) as { type: string; run: { id: string; status: string } };
// ... handle event.type ...
return new Response(null, { status: 204 });
});The TypeScript SDK (preview) exposes the same check as client.verifyWebhookSignature(secret, header, body).
#Reliability notes
- Respond
2xxquickly (ideally under a couple of seconds) and do any slow work afterwards — a delivery is only ever retried on a failure response, not a slow one, but a slow handler still risks the sender's own timeout. - A delivery's target is re-validated on every attempt, not just at registration — DNS changing between attempts (e.g. to point at an internal address) doesn't bypass the public-URL check.
- A
3xxresponse is treated as a failed attempt, not followed — a webhook endpoint can't use its own redirect to retarget a delivery after the URL check has already passed. - Deliveries are best-effort, not exactly-once: design your handler to be idempotent on
run.id+type, since a retried attempt (or, rarely, a redelivered one) can arrive more than once.