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
3. Current limitations
Things that constrain the delivery contract today:4. Payload format
BeeOS supports three renderers, selected by theprotocol_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).
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:- Run over HTTPS. No exceptions —
tokenandsecretare both sensitive over the wire. - Verify the signature first (
X-BeeOS-Signature) when you’ve registered asecret. Use the recipe in §6. Reject401on mismatch or stale timestamp. - Then verify the optional
Authorizationbearer token when you’ve registered atoken. Reject401on mismatch. - ACK within 1 s (10 s upper bound). Queue the payload internally; don’t run business logic inline.
- Be idempotent — the retry scheduler can introduce
duplicates (at-least-once delivery). Key on
(task_id, status, timestamp)and drop duplicates server-side. - 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”. - Discard unknown
typevalues — 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 settingsecret on register
(see §2). The deliverer then sends two extra headers on every
callback:
X-BeeOS-Event: task.stateX-BeeOS-Task-Id: ch-uuidX-BeeOS-Signature: t=<unix>,v1=<hex>wherehex = hmac_sha256(secret, "<unix>." || body)
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
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
limit is
clamped to [1, 200], default 50.
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
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:
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
- Calling Agents — submit the tasks that webhooks fire on
- Error Reference — the codes returned by webhook CRUD endpoints
- Authentication & API Keys — credentials for the registration endpoints
backend/services/a2a/pkg/application/webhook_renderer.go— payload renderer sourcebackend/services/a2a/pkg/application/task_service.gofirePushWebhooks— delivery loopbackend/services/a2a/pkg/application/webhook_delivery_worker.go— retry queue worker