Skip to main content
Webhooks let you receive task lifecycle updates by POST instead of polling. Register a callback URL on a task and BeeOS will hit it whenever the task transitions state.
Status — P2-A fully landed: callbacks are (1) signable with HMAC-SHA256 (X-BeeOS-Signature), (2) retried on failure with the documented exponential backoff schedule (1m → 5m → 30m → 2h → 12h, then dead-letter), and (3) auditable + replayable through GET .../deliveries and POST .../deliveries/{id}/redeliver.
The single-attempt 10 s timeout still applies per attempt, but failed attempts now survive a process crash and are picked up by the background retry worker.

1. Lifecycle


2. Registering a webhook

Request

Response (201)

token and secret are never returned in any list / get response — once registered they’re only used server-side. The has_secret boolean lets the UI / SDK surface “signing enabled” without exposing the secret itself. To rotate a secret, Set the webhook again with a fresh value (passing "" leaves the existing secret untouched — use DELETE + re-register to clear).

Listing / deleting

A task can hold any number of webhooks (no explicit cap today). Deletion is immediate — no in-flight delivery cancellation, but no new attempts after delete.

3. Current limitations

Things that constrain the delivery contract today:

4. Payload format

BeeOS supports three renderers, selected by the protocol_filter column at registration time. Webhooks registered via this OpenAPI endpoint always use the openapi renderer.

openapi renderer — TaskEvent envelope

Matches the SSE-stream payload from GET /tasks/{id}/events so you can share decoders. Emitted on every status transition (intermediate and terminal).
Possible status values: queued, running, input_required, auth_required, completed, failed, canceled, timeout, rejected (see Calling Agents §3e). final: true only on terminal states.

a2a and generic renderers

Used by A2A JSON-RPC clients and by generic / legacy subscribers that registered without a protocol filter. Not selectable from the OpenAPI Gateway — documented for reference only. See webhook_renderer.go.

5. Receiver checklist

A production-grade receiver for the current delivery semantics needs to:
  1. Run over HTTPS. No exceptions — token and secret are both sensitive over the wire.
  2. Verify the signature first (X-BeeOS-Signature) when you’ve registered a secret. Use the recipe in §6. Reject 401 on mismatch or stale timestamp.
  3. Then verify the optional Authorization bearer token when you’ve registered a token. Reject 401 on mismatch.
  4. ACK within 1 s (10 s upper bound). Queue the payload internally; don’t run business logic inline.
  5. Be idempotent — the retry scheduler can introduce duplicates (at-least-once delivery). Key on (task_id, status, timestamp) and drop duplicates server-side.
  6. Poll GET /tasks/{id} periodically for any task whose terminal-state webhook you must not lose (e.g. billing-relevant). The webhook is “fast notification”; the API is “truth”.
  7. Discard unknown type values — additions to the renderer are additive (extra fields / new types) per ADR-0017; never crash on a new event type.

6. HMAC signing (P2-A)

Opt into HMAC-SHA256 body signing by setting secret on register (see §2). The deliverer then sends two extra headers on every callback:
  • X-BeeOS-Event: task.state
  • X-BeeOS-Task-Id: ch-uuid
  • X-BeeOS-Signature: t=<unix>,v1=<hex> where hex = hmac_sha256(secret, "<unix>." || body)
Other headers (X-BeeOS-Webhook-Format, the optional Authorization: Bearer <token>) are unchanged. You can use signing in addition to the bearer token during rotation.

Verification recipe — Python

Verification recipe — Node.js / Express

Critical: hash the raw request bytes, not a re-serialised JSON object. Re-serialising changes whitespace / key order and the HMAC will not match.

Why the ±5 min skew check?

Without a timestamp check, an attacker who captures one valid callback can replay it forever. Rejecting old or future-dated timestamps narrows the replay window. ±5 minutes is BeeOS’s recommendation; receivers in closed networks may go tighter.

7. Delivery audit log + manual replay (P2-A part 3)

Every callback attempt is now recorded as a durable row that you can list and replay through two REST endpoints. The rows survive process crashes (the retry worker picks them up after the lease expires), so a transient deploy / rolling restart no longer drops in-flight callbacks.

Retry schedule

After a failed attempt the row is rescheduled per a fixed backoff: A row stays in pending between attempts (the worker flips it back to pending from failed when the schedule fires). Manual replay via POST .../redeliver clones the row into a fresh pending attempt regardless of how many automatic retries already ran.

Lifecycle states

GET .../deliveries

Returns the most recent delivery rows, newest first. limit is clamped to [1, 200], default 50.
The raw payload bytes, bearer token, and HMAC secret are NEVER returned — only diagnostic fields. If you need to verify what the receiver should have seen, consult your own log of the originating task transition; the renderer field tells you which payload schema applied.

POST .../redeliver

Clones a failed or dead_letter row into a fresh pending row. The clone re-fires the exact payload bytes (and HMAC signature) the receiver should have seen — receivers can treat manually-replayed callbacks identically to automatic retries. Returns 202 with the new pending row:
Errors: Each redeliver call enqueues a NEW pending row — repeated calls produce repeated deliveries. Treat this endpoint as a manual rescue, not a caller-side retry-on-network-error.

See also