Log in Create workspace
Skip to content

Platform

Webhook delivery, signing, and retries

Receive platform events without polling. A matching subscription sends an HTTP POST to your URL; your receiver verifies its signature, acknowledges durable receipt, and safely handles retries.

API docs
On this page

Start here

React without polling: preserve the raw body, verify the signature, acknowledge promptly, and make processing safe across retries.

Before you begin

  • A publicly reachable HTTPS receiver allowed by the outbound URL policy
  • A caller-supplied secret or a plan to capture rotate_secret output
  • Durable delivery-ID storage

Put it into practice

  1. Create and test the subscription

    Choose the event types, then either create through the CSRF-authenticated operator API with a secret you already stored, or create first and call rotate_secret once to capture its returned secret. Send webhook.test afterward.

  2. Verify before parsing

    Build <timestamp>.<raw body>, calculate the lowercase HMAC-SHA256 digest, and compare it with x-agentops-signature in constant time. Reject a mismatch before doing work.

  3. Acknowledge and process safely

    Return success after durable acceptance, then process asynchronously. Deduplicate by delivery id.

What success looks like

A test delivery arrives, passes signature verification, is acknowledged once durably accepted, and produces one application action even if the platform retries it.

01 Delivery payload

details

POST body fields

Each delivery POSTs JSON with id, event_type, workspace_id, occurred_at, data, and delivery_attempt. Test deliveries from POST .../test also set test: true. This is not the realtime Events envelope (which uses type and has no delivery_attempt). Headers are x-agentops-event, x-agentops-timestamp (Unix seconds), and x-agentops-signature (HMAC-SHA256).

02 Signature verification

example

HMAC-SHA256

1) Concatenate timestamp UTF-8 bytes plus b'.' plus the raw body bytes; never reserialize JSON. 2) Compute HMAC-SHA256 using the subscription's signing secret as the key. 3) Hex-encode the result (lowercase). Always use constant-time comparison to prevent timing attacks.

python
# Python example
import hmac, hashlib

def verify_signature(timestamp, body, secret, signature):
    message = timestamp.encode("utf-8") + b"." + body
    expected = hmac.new(
        secret.encode("utf-8"), message, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

03 Retry strategy

details

Exponential backoff

Transport failures and HTTP 408, 425, 429, or 5xx responses retry with 30 * 2^(attempt - 1) second backoff, capped at 30 minutes and 8 attempts. Other 4xx responses, blocked outbound URLs, and invalid signing secrets fail without retry. Automatic redirects are disabled. Each attempt has a 10-second overall deadline (connect and response); a receiver that answers later counts as a transport failure and is retried. Acknowledge with a 2xx immediately and process asynchronously. Only the first 4 KB of the response body is read, and up to 2,000 characters are stored as last_response_body / last_error.

04 Event types

details

Published webhook events

Subscriptions can specify individual event types or use wildcard *. Realtime.Events always fans matching publishes into webhook delivery. Shipped types include command lifecycle; trace.created, trace.event.appended, and trace.finalized; replay started/updated/completed/failed/stuck_detected; approval_request created/decided/executed; eval_run.completed; fleet lifecycle plus fleet.batch_operation.created/progress/completed; failure_group.created; webhook.test; security.violation.created, security.violation.resolved, and security.report.generated; and gateway.frozen / gateway.thawed.

failure_group.anomaly_detected is not a webhook event

That string is an alert-rule trigger. When it fires the platform publishes alert.created to destinations. The webhook and realtime event for a new group is failure_group.created (failure_group_id, trace_id, signature, severity).

05 Alert destinations and rules

details

Create a destination before a rule

A workspace owner or admin opens /app/admin and creates an alert destination of type webhook, slack, discord, email, or pagerduty. Webhook, Slack, and Discord need a webhook_url; email needs a valid recipient_email; PagerDuty needs a routing_key. Invalid or missing targets fail validation. These are alert transports, distinct from signed event webhook subscriptions.

Test delivery, then bind a rule

Use Send test on the destination to queue an alert.test delivery through the production worker, then inspect the delivery status rather than treating enqueueing as receipt. Create a named generic event rule with event_type, severity, optional deduplication window, and selected destination IDs; or choose failure-group anomaly and set min_occurrences, window_minutes, and min_failure_severity. Rule creation and destination links are atomic: an unknown or foreign destination rolls the rule back. Owners/admins manage this page; rules dispatch only when a matching event occurs and the destination can deliver.

06 Operator console

details

/app/admin

Workspace admin can create subscriptions, send a test delivery (event_type webhook.test, test true), list recent attempts, filter failures, and redeliver them. The console never reveals or rotates the signing secret. For a usable receiver, either create through POST /api/v1/operator/webhook_subscriptions with a caller-chosen signing_secret, or create first and make the CSRF-authenticated POST .../:id/rotate_secret call, which returns the new value once. Create and list responses omit signing_secret.

Delivery history retention

Finished (succeeded or failed) deliveries are kept for 30 days and then pruned automatically; pending deliveries are never pruned. A deletion request with dataset "all" under Data controls (POST /api/v1/operator/deletion_requests, optional older_than_days) also removes the workspace's delivery history, which contains the event payloads that were sent. Any non-delivering record can be redelivered within that window; a delivering record returns 400 in_flight. Successful-delivery replays can duplicate receiver side effects, so deduplicate by event ID.

Troubleshooting

Every signature comparison fails
Verify the exact raw bytes, not re-serialized JSON, and include the timestamp plus period. Confirm the receiver has the caller-supplied secret or the latest one-time rotate_secret result; create and list responses never reveal a generated secret.
Deliveries stop retrying
Only transport failures, 408, 425, 429, and 5xx responses retry. Responses slower than 10 seconds are retried as transport failures. Fix other 4xx responses, blocked URLs, or secret configuration before requesting another test.

Where to go next

see also