Webhooks as an audit layer: signed events for agent observability
The verify response tells your integration what was decided. Webhooks tell everything else — your SIEM, your alerting system, your audit database — asynchronously and in real time. Here's how BehalfID's outbox-backed delivery model works.
The verify response is synchronous — your integration gets the decision before the executor runs. But not every system that cares about agent decisions is in the request path. Your security tooling, alerting pipeline, compliance log, and audit database all need to know what happened, and they shouldn't have to poll.
That's what webhooks are for. BehalfID emits a signed event for every verify decision — verification.allowed, verification.denied, or verification.requires_approval — and delivers it to your configured endpoint through an outbox-backed retry system.
The event payload
Each webhook event carries the same information as the verify response, plus routing metadata:
{
"eventId": "evt_01hx…",
"type": "verification.denied",
"createdAt": "2026-05-10T14:22:08.412Z",
"data": {
"requestId": "req_01hvz8…",
"agentId": "agent_ollie",
"decision": "denied",
"reason": "No active purchase permission",
"action": "purchase",
"vendor": "coachella.com",
"amount": 742,
"riskLevel": "medium"
}
}eventId is unique per event and stable across delivery retries — use it to deduplicate. requestId links back to the verify call that produced this event and to the audit log entry.
Signature verification
Every event is signed with HMAC-SHA256 over timestamp.rawBody using the derived key from your whsec_ secret. Verify the signature before processing any event — never trust an unsigned or unverified payload.
import { verifyWebhookSignature } from "@behalfid/sdk";
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("behalfid-signature") ?? "";
const timestamp = req.headers.get("behalfid-timestamp") ?? "";
const event = verifyWebhookSignature({
body,
signature,
timestamp,
secret: process.env.BEHALFID_WEBHOOK_SECRET!
});
// event is now verified — safe to process
if (event.type === "verification.denied") {
await alertingPipeline.send(event.data);
}
return new Response("ok", { status: 200 });
}200as soon as you've verified and enqueued the event. Do not block the webhook response on downstream processing — slow receivers trigger retries.Delivery guarantees and retries
BehalfID writes events to an outbox before delivering them. If your endpoint is unreachable or returns a non-2xx response, delivery is retried up to five times with a capped backoff. After five failures, the event moves to dead-letter state.
- At-least-once delivery. An event may be delivered more than once — on retries, or after a manual replay. Always deduplicate by
eventId. - Dead-letter replay. Dead-lettered events are visible in the console and can be replayed manually. Useful when your receiver was down during a burst of denials you care about.
- No ordering guarantee. Events are delivered roughly in order but retry jitter can cause out-of-order arrival. Use
createdAtto reconstruct sequence if needed.
What to build on top of it
Webhooks are the right hook for anything that needs to react to agent decisions outside the request path:
- Alerting. Page on-call when a high-risk action is denied or when a single agent produces an unusual spike of denials within a window.
- Compliance logging. Pipe every
verification.*event to an append-only audit store with therequestId,eventId, agent, action, and timestamp. Immutable records per decision. - Human-in-the-loop queues. On
verification.requires_approval, push the event to a review queue where a human can approve or deny before the agent is unblocked. - Revocation triggers. If an agent produces a pattern of high-risk denials, automatically disable the agent or revoke a specific scope.
The verify API handles the decision. Webhooks handle everything that needs to react to it.