Webhooks and Events for AI Agents
The platform has several webhook and event surfaces. They are related, but they serve different purposes.
Event taxonomy
| Surface | Direction | Audience | Purpose |
|---|---|---|---|
| Legacy browser task webhook | the platform to your URL | API developers | Receive completion, failure, or cancellation for a browser-use task. |
| Signed pending-action events | the platform to your URL | API developers | Receive durable approval/input lifecycle events with bounded retries. |
| Inbound agent webhook | Your system to the platform | Developers and operators | Trigger or message a specific agent from an external system. |
| Provider webhooks | Provider to the platform | Operators | Receive email, status, open/click, or provider events. |
| Outbound webhook tool | the platform to your URL | Advanced users/developers | Let an agent send a webhook as part of its work. |
Only depend on the surfaces documented for your use case. Provider and console implementation routes are not general-purpose public APIs.
Legacy browser task callbacks
Add an optional webhook URL when creating a browser-use task to receive a callback when the task finishes or is canceled.
URLs must be http or https. Use a secret token or path segment in the callback URL if your receiver needs authentication. This task API does not configure a signing secret or custom authentication headers. Reconcile the reported task ID and result with the authenticated task API when needed.
Agent-scoped task route:
curl -X POST https://your-deployment.example/api/v1/agents/browser-use/{agentId}/tasks/ \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Find pricing",
"webhook": "https://example.com/hooks/agent-platform?token=example"
}'
User-level task route:
curl -X POST https://your-deployment.example/api/v1/tasks/browser-use/ \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Find pricing",
"webhook": "https://example.com/hooks/agent-platform?token=example"
}'
Response fields include the stored webhook plus delivery metadata such as webhook_last_called_at, webhook_last_status_code, and webhook_last_error.
Delivery semantics
- At most one admitted delivery attempt when status becomes
completed,failed, orcancelled; callbacks attached to a persistent agent remain subject to its emergency dispatch fence. - POSTs JSON with
Content-Type: application/jsonandUser-Agent: <product-name>-AgentWebhook/1.0, using the installation's product name. - Uses a 10 second timeout.
- Does not retry non-2xx responses in the current task callback path.
- Records the last delivery attempt on the task.
Payload examples
Success:
{
"id": "task-uuid",
"status": "completed",
"agent_id": "agent-uuid",
"result": {"summary": "Found pricing."}
}
Failure:
{
"id": "task-uuid",
"status": "failed",
"agent_id": "agent-uuid",
"result": null,
"error_message": "Timed out waiting for page."
}
Cancelled:
{
"id": "task-uuid",
"status": "cancelled",
"agent_id": "agent-uuid",
"result": null,
"message": "Task has been cancelled."
}
Signed agent events
Create an outbound webhook through POST /api/v1/agents/{id}/webhooks/ with name, url, and event_types. Read-only keys may list webhook metadata; secret-bearing detail, creation, update, deletion, and rotation require agents:manage.
The legacy subscription values are agent.message, agent.pending_action.created, agent.pending_action.resolved, agent.pending_action.expired, agent.pending_action.*, and *. Omitting event_types defaults to agent.message; an empty array subscribes to no automatic events.
The two agent delivery paths differ. Pending-action events are emitted by the system with the versioned envelope below and durable retries. agent.message is the event header used when the agent explicitly calls send_webhook_event; its body is the JSON object supplied by that tool. It is not an automatic subscription to every email, chat message, or timeline event, and the tool delivery path does not use the durable retry queue. Deployments with automatic events enabled additionally offer the distinct automatic types described below; timeline/request reads remain reconciliation surfaces.
System event example:
{
"id": "e46d0cbf-32e6-4a0b-9146-c96ca48cff26",
"type": "agent.pending_action.created",
"version": "1",
"occurred_at": "2026-09-06T12:00:00+00:00",
"agent_id": "b5f0f2c8-1c2b-4bb5-836b-9b4e2f86b743",
"data": {
"pending_action": {
"id": "human_input:0b1fbb92-c018-4814-a57f-c80b24e3eb87",
"type": "human_input",
"status": "pending",
"created_at": "2026-09-06T12:00:00+00:00",
"expires_at": null,
"data": {"question": "Which environment?", "options": [], "input_mode": "free_text_only"},
"allowed_decisions": ["answer", "dismiss"]
}
}
}
Resolved/expired payloads also include data.resolution.status. See Pending actions for each action's fields and decisions.
Both agent delivery paths send these headers:
| Header | Value |
|---|---|
X-DT-Event-Id | UUID identifying the delivery's logical event |
X-DT-Event-Type | Concrete event type |
X-DT-Event-Version | 1 |
X-DT-Signature | t=<unix-seconds>,v1=<hex-HMAC-SHA256> |
The X-DT-* names are literal protocol headers even on a differently branded deployment. Use these names in webhook receivers for both initial deliveries and retries.
Creation, secret rotation, and detail return signing_secret; lists omit secrets and destination URLs. Rotating a signing secret replaces it immediately. There is no dual-secret grace period in the sender; update your receiver's stored secret accordingly.
Verify the signature
Calculate HMAC-SHA256 with the UTF-8 signing secret over the ASCII timestamp, one period, and the original request-body bytes. Do not parse and reserialize JSON before verification. Pass the value of the receiver's X-DT-Signature header as signature_header in the following examples. This receiver example uses a five-minute freshness tolerance; tune that receiver policy to your clock synchronization and delivery environment.
import hashlib
import hmac
import time
def valid_agent_signature(raw_body, signature_header, secret, *, now=None):
try:
fields = dict(part.split("=", 1) for part in signature_header.split(","))
timestamp = fields["t"]
received = fields["v1"]
if not timestamp.isascii() or not timestamp.isdigit():
return False
if len(received) != 64 or any(c not in "0123456789abcdef" for c in received):
return False
timestamp_value = int(timestamp)
except (ValueError, KeyError):
return False
if abs((time.time() if now is None else now) - timestamp_value) > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
timestamp.encode("ascii") + b"." + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received)
Verify first, then durably accept the event before performing business effects. For signed system envelopes, deduplicate on the signed body's id and check it matches X-DT-Event-Id. Repeated IDs should receive a successful acknowledgment after you confirm they were already accepted.
For a minimal durable inbox, initialize a table once and acknowledge only after the acceptance transaction commits. A separate consumer can read unprocessed inbox rows and apply its own business-effect deduplication. This example handles signed system envelopes, including automatic events; explicit tool payloads need their own signed business identifier.
import json
import sqlite3
import uuid
# Run once on receiver startup. Use a durable disk path and back up this inbox.
with sqlite3.connect("events.sqlite3") as db:
db.execute("CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY, body BLOB NOT NULL)")
def accept_system_event(raw_body, signature_header, secret):
if not valid_agent_signature(raw_body, signature_header, secret):
return 401
try:
envelope = json.loads(raw_body)
event_id = str(uuid.UUID(envelope["id"]))
if envelope["version"] != "1" or not envelope["type"].startswith("agent."):
return 400
except (ValueError, KeyError, TypeError, AttributeError):
return 400
with sqlite3.connect("events.sqlite3") as db:
db.execute("INSERT OR IGNORE INTO inbox (id, body) VALUES (?, ?)", (event_id, raw_body))
existing = db.execute("SELECT body FROM inbox WHERE id = ?", (event_id,)).fetchone()
if bytes(existing[0]) != raw_body:
return 409 # Same signed identity with different bytes is a conflict.
return 204 # Committed new/duplicate acceptance, safe under concurrent calls.
The signature covers the timestamp and body, not the separate event-ID/type headers. For explicit tool sends with a raw payload, include a stable business-event identifier inside that signed payload if your consumer needs deduplication; the header alone does not authenticate that identifier.
System-event delivery and recovery
Pending-action delivery uses a 15-second HTTP timeout and at most five attempts. Failure delays are 60, 300, 1,800, and 7,200 seconds. Any final 2xx response marks delivery successful; known non-2xx/proxy failures use the bounded retry path; transport exceptions retain unknown and require deliberate recovery. A retry keeps the event ID and body while generating a fresh signature timestamp.
Process expensive work after acknowledging receipt. Deliveries may repeat, and independently queued events have no documented ordering guarantee. Use occurred_at for context and fetch current pending actions before deciding. Scoped delivery inventories expose exhaustion, sanitized attempts and deliberate recovery as described below. Reconcile your queue with GET /pending-actions/ after outages.
The explicit send_webhook_event tool uses a 15-second timeout and reports its single attempt back to the agent. Legacy browser task callbacks are the separate unsigned, single-attempt contract documented above.
Inbound agent webhooks
Inbound agent webhooks let an external system send an event into a specific agent. Use this when your own system should trigger an agent without going through a human chat channel.
Keep payloads concise and include enough context for the agent to act safely. If the event can cause external side effects, design the agent's charter so it asks for approval before acting.
Create, disable, rotate, and delete inbound endpoints through the Agent API webhook management endpoints. Creation and secret rotation return the complete invocation URL. Treat that URL as a credential: webhook lists omit it, and retrieving it requires an API key with agents:manage.
Invoke the returned URL as-is. Its path is /api/v1/webhooks/inbound/agents/{webhookId}/?t=<secret>. Invocation authenticates with that URL secret, not X-Api-Key. JSON, plain text, URL-encoded forms, and multipart uploads are accepted; the body need not follow a fixed event schema. Query parameters other than t are included in the event context. Attachments are subject to file quarantine.
curl --fail-with-body -X POST "$AGENT_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"event":"deployment.completed","deployment_id":"deploy-123"}'
A successful invocation returns HTTP 202:
{
"accepted": true,
"webhookId": "371d980e-3a19-43e1-b7ba-8d44ff71c599",
"webhookName": "Deployment completed",
"messageId": "a6da0d45-57f3-4d27-a48b-76497fc956f2",
"queued": true,
"receivedAt": "2026-09-06T12:00:00+00:00"
}
202 acknowledges ingestion, not completed agent work or a confirmed broker publication. During an emergency halt, the platform can retain the message and return queued: false with processingBlockedReason: "agent_emergency_halted". Preserve messageId and investigate the hold; resubmitting does not release it and can duplicate ingress. queued: true is not proof that a worker has started or that all execution policies permit continuation.
Missing secret or malformed JSON returns 400; invalid secret returns 403; an unknown webhook or wrong execution environment returns 404; disabled webhook or inactive agent returns 409; ingestion failure returns 500. Error bodies include accepted: false with error or lifecycle-specific fields.
Invocation has no built-in Idempotency-Key replay or source-event deduplication. Retrying an uncertain request can add another message. Include your source event ID and enforce duplicate handling in the sending system and workflow. Secret rotation invalidates the previous invocation URL immediately.
Provider webhooks
The platform uses provider webhooks for email, status, open/click, and related delivery events. These routes are for provider integration and deployment configuration, not arbitrary application callbacks.
Installation operators should configure provider routes only with the expected provider secrets and network exposure.
Outbound webhook tool
Where enabled, an agent can call an outbound webhook as a tool. Treat this like any other external write:
- Use an endpoint you control.
- Validate the incoming request.
- Keep payloads scoped.
- Require approval before sensitive external actions.
- Log received events for debugging.
Protected inbound retries (gated)
With extended retry admission enabled, an inbound invocation may send Idempotency-Key as its source-event identifier. The namespace is the durable webhook ID, so an authorized URL-secret rotation preserves it; the old URL still fails authentication. Without the header, invocation keeps its existing non-deduplicated behavior. Identical retries return the original 202 body, messageId, initial processing eligibility and Idempotency-Replayed: true. This is historical acceptance, not evidence that a worker ran. Default retention is seven days, configured separately by API_INBOUND_RETRY_RETENTION_DAYS, and acceptance discloses idempotencyExpiresAt plus Idempotency-Expires-At. Expired retries can create new input.
Canonical inputs include method/content type, parsed JSON or form fields, meaningful query parameters other than t, and attachment field/name/media type/content hashes. JSON object ordering and multipart boundaries are ignored. Arrays, repeated form/query values and files within each field preserve order. Duplicate JSON keys and non-finite JSON numbers are rejected before protected intake. Supported protected content types are JSON, plain text, URL-encoded form and multipart form. Attachment hashing is streamed, rewound before intake and bounded by the smaller of the installation file limit and 25 MiB total. An identical retry creates no second attachment or quarantine intake. A changed input returns 409 idempotency_conflict.
Disabled/deleted webhooks, invalid secrets, inactive/deleted agents, unavailable owners and wrong execution environments still deny replay. Emergency-held acceptance stores history and a blocked publication intent; it never automatically resumes after reset. Publication recovery retains the original message, owner, queue and generation. Source content/quarantine retention follows its existing policy independently of receipt expiry.
Inbound/outbound secret rotation endpoints also support gated member-key replay. Keep the exact request/key to recover the issued result; a newer rotation returns 409 rotation_superseded. Retrieve current metadata rather than treating an old secret as current. See Idempotency and retries for deployment, outcome lookup and retention.
Protected attachment admission currently requires both default and malware-quarantine storage to use Django's local FileSystemStorage. New protected uploads on a remote/custom storage backend return 503 idempotent_attachment_storage_unavailable before receipt/message/intake creation; retained identical retries remain readable. Remote object-store prestaging and cleanup must be implemented and validated before enabling that content profile, because the existing intake may write/promote blobs inside the message transaction. JSON, text and attachment-free forms do not have this restriction. Local physical blob writes still follow existing orphan/quarantine cleanup; only database intake rows share the atomic receipt/message/publication commit.
Automatic events, catch-up and delivery recovery
Automatic capture is a deployment-gated preview: API_AUTOMATIC_AGENT_EVENTS_ENABLED defaults off. On deployments with the gate enabled, subscriptions can select agent.message.created, agent.request.updated, and agent.request.responded. These are distinct from explicit agent.message tool sends. Version 1 envelopes contain a stable logical id, type, occurred_at, captured_at, agent_id, source reference/revision and safe data. New payloads contain references, state/reason codes and availability markers; they exclude bodies, thinking, tool input/output, caller metadata, credentials and protected evidence. Fetch authorized content separately.
Request updates represent committed application-request observation revisions; unchanged polling produces no event. Responded events require explicit consumed-input/reply lineage and never assert successful external work. A shared reply produces a separate request-scoped event for each proven link, including its message event. General channel messages have no service submission lineage.
API-created subscriptions bind to the submission principal. Services may subscribe only to the three automatic types for their own requests, using current agent/Team grants and agents:manage; wildcard/pending-action subscriptions are unavailable to services. request_visibility defaults to principal; optional request_filter is a visible request UUID and excludes general channel messages without that request lineage. A human owner/Organization administrator may explicitly authorize/audit request_visibility: "all". An administrator cannot widen a service-owned subscription. Agent visibility does not expose foreign request IDs.
Operation (relative to /api/v1) | Authority | Result |
|---|---|---|
GET /agents/{id}/events/ | agents:read, submission-principal scope; human inspect=true explicitly audits administration | Retained metadata, safe automatic envelopes, sequence, cursor, next_cursor, reconciliation URLs |
GET /agents/{id}/webhooks/{webhook_id}/deliveries/ | agents:read and current subscription management authority | Cursor page, scoped pending/unknown/failed counts and oldest pending age |
GET /agents/{id}/webhooks/{webhook_id}/deliveries/{delivery_id}/ | Same authority and source-principal scope | Sanitized attempts/recovery summaries, revision/cycle and recovery eligibility |
POST /agents/{id}/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver/ | agents:manage, subscription authority, original emergency generation, retained source and required Idempotency-Key | Audited new cycle, HTTP 202, replay receipt headers |
Event queries accept cursor, limit (1–100), inspect (default false), concrete type, and request_id. Delivery pages accept cursor/limit/inspect; detail/recovery accept inspect. Scoping precedes pagination/counts. Diagnostic responses omit receiver bodies, internal exceptions, destination URLs and secrets. Drain next_cursor, then poll with the returned cursor, including after empty pages. Per-agent sequences establish catch-up order, not HTTP arrival order; fetch current request state when revisions arrive out of order.
curl --fail-with-body -X POST "$API_BASE/agents/$AGENT_ID/webhooks/$HOOK_ID/deliveries/$DELIVERY_ID/redeliver/" \
-H "X-Api-Key: $API_KEY" -H "Idempotency-Key: repair-incident-123" \
-H "Content-Type: application/json" \
-d '{"expected_revision":7,"reason":"Receiver outage repaired","acknowledge_duplicate_risk":true}'
Known non-2xx/proxy failures receive at most five attempts per cycle, delayed 60, 300, 1,800 and 7,200 seconds. Transport errors, lost acknowledgements and expired worker claims become unknown without automatic resend. Concurrent live claims do not poison a send. Explicit failed/unknown recovery requires expected revision and reason; unknown additionally requires acknowledge_duplicate_risk: true. Concurrent recovery cannot create two cycles. Prior admissions/results/recovery evidence remain immutable; event ID/body/occurrence time stay stable. Existing 24-hour member-key and service-identity replay namespaces apply, with current authority/source/generation rechecked even on response replay.
Delivered/cancelled/purged work, revoked/changed subscriptions, transferred/archived agents and stale generations cannot be recovered. Reset never refreshes an old generation. Destination/filter/visibility changes advance configuration_revision and cancel queued historical work. Admitted sends retain their snapshot and cannot be recalled. A hold that lands while a send is in flight makes the platform close its side of that connection, but the receiver may already have read the request, so the delivery reads unknown and is never resent automatically; a hold that lands after the claim but before the first byte sends nothing and cancels the delivery. Closing a connection is evidenced only where the deployment's socket hooks and watcher run, and is not guaranteed. This release has no replay to a replacement destination; reconcile authorized events/requests. Signing-secret rotation remains immediate: coordinate receiver cutover; later permitted attempts use the current secret and fresh timestamp/signature. The settings dialog exposes event and explicit administrator-visibility choices.
Automatic envelope retention defaults to seven days, configurable 1–7 days and bounded earlier by message/request content or artifact retention. Tombstones remove encrypted bytes and cancel pending sends without changing source identity or delivering a replacement body. Gap-crossing/expired cursors return 410 event_cursor_expired with reconciliation URLs; foreign/invalid/filter-mismatched cursors return 400. A fresh inventory starts a new reconciliation boundary. Historical events are not inferred from timeline records.
Apply 0581–0583, drain old writers/dispatchers, and upgrade source writers/readers/recovery workers and Beat before validating and activating receivers. Pre-upgrade deliveries lack original owner/destination snapshots: pending rows become legacy_authority_unqualified cancellations; interrupted sends remain unknown. Existing bodies/IDs stay unchanged and sanitized history remains readable, with no fabricated source lineage/attempt evidence or inferred recovery authority. New compatible pending-action sources retain their original envelopes and retention contract.
API_AGENT_EVENT_DELIVERY_PAUSED=true pauses new dispatch admissions independently from capture, preserving reads/history/recovery intent. Capture disablement preserves authorized recovery, subscription metadata edits/removal and compatible pending-action capture, recording observed stream boundaries. Adding new automatic subscription types remains gated. Coordinate flags across writers; a transition with no observing process cannot attest its exact wall-clock boundary. Pause/halt/edit cannot recall an admitted HTTP request. Rollback keeps schemas, evidence, readers/maintenance and claim/revision fences; older dispatchers cannot safely handle new cycles.
Reconciliation repairs queue outages from durable delivery rows. Completed provider/broker observations with failed capture retain frozen revision metadata for repair without repeating the external effect. New request admission/direct replies instead require capture in their local transaction. Unit and lock checks do not validate receiver delivery, worker/Beat recovery or emergency containment.
Application request-updated events can add data.result metadata (state, safe categorical error code, schema hash/profile and immutable artifact UUID/version/checksum/size). They never include schemas or native result content. This additive metadata uses the existing request revision, submission-principal filters, retention and delivery gates. Retrieve current native content through the authorized result route.