Documentation
Webhooks
Subscribe to verification, agent, and permission events. BehalfID signs each event and delivers through a durable outbox.
What webhooks are for
Webhooks push signed events to your HTTPS endpoint when agents, permissions, or verification decisions change. Use them to sync SIEM tools, open tickets on denials, pause CI when production deploy approval is required, or mirror audit activity into your own store.
Events are written to an outbox before the API response returns. Delivery runs asynchronously via /api/webhooks/process, so a down receiver does not block verify() or permission mutations.
Event types
verification.allowedverification.deniedagent.createdagent.disabledagent.enabledagent.key_rotatedpermission.createdpermission.revokedSubscribe to a subset when you create the endpoint in the dashboard webhooks page. Only subscribed types are delivered.
Create an endpoint
- Open Dashboard → Webhooks and add an HTTPS URL.
- Select the event types you want to receive.
- Copy the one-time signing secret (
whsec_…). BehalfID stores only a derived hash and a short preview — the full secret cannot be viewed again. - Store the secret as
BEHALFID_WEBHOOK_SECRET(or equivalent) in your receiver environment.
Production URLs must use https://. Local http://localhost endpoints are allowed only in development. Rotating the secret immediately stops the previous secret from verifying new deliveries.
Payload
{
"eventId": "evt_xxx",
"type": "verification.allowed",
"createdAt": "2026-05-02T00:00:00.000Z",
"accountId": "acct_xxx",
"data": {
"requestId": "req_xxx",
"agentId": "agent_xxx",
"action": "access_data",
"allowed": true,
"risk": "low",
"permissionId": "perm_xxx"
}
}Payloads never include API keys, setup tokens, webhook secrets, or newly rotated agent keys. Treat eventId as the dedupe key.
Headers
BehalfID-Event-ID— stable event ID (same as payloadeventId).BehalfID-Timestamp— Unix seconds included in the HMAC base string.BehalfID-Signature—v1=<hex_hmac>overtimestamp.rawBody.
Verify against the exact raw JSON body your server received. Do not re-serialize parsed JSON before checking the signature — whitespace and key order must match.
Verify with the SDK
import { verifyWebhookSignature } from "@behalfid/sdk";
export async function POST(request: Request) {
const rawBody = await request.text();
const valid = await verifyWebhookSignature({
secret: process.env.BEHALFID_WEBHOOK_SECRET!,
payload: rawBody,
timestamp: request.headers.get("behalfid-timestamp") ?? undefined,
signature: request.headers.get("behalfid-signature") ?? undefined
});
if (!valid) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody) as { eventId: string; type: string };
// Deduplicate by event.eventId, then handle side effects idempotently.
return new Response("ok");
}The helper rejects timestamps outside a 300-second skew window by default (toleranceSeconds). If your deployment sets BEHALFID_WEBHOOK_SIGNING_PEPPER, pass the same value as signingPepper to the SDK helper.
Retries, DLQ, and replay
Delivery is at least once. Failed deliveries retry with bounded exponential backoff, then move to a dead-letter state:
attempt 1 → immediate attempt 2 → +5 seconds attempt 3 → +30 seconds attempt 4 → +2 minutes attempt 5 → +10 minutes (after 5 failures → deadLetter = true)
Inspect failed events and delivery attempts from the webhook detail page in the dashboard. After fixing the receiver, replay a dead-lettered event — replay resets status to pending, clears lastError, and sets attempts back to zero. Events that are still pending, processing, or completed cannot be replayed.
Local testing
npm --prefix examples/webhook-receiver install BEHALFID_WEBHOOK_SECRET=whsec_xxx npm --prefix examples/webhook-receiver start
Point a development endpoint at http://localhost:4000, trigger a verification, then process the outbox (hosted deployments usually schedule /api/webhooks/process via cron). See also the SDK webhook helper and the Concepts page for how verification decisions relate to these events.