Event format
The JSON event AgentOwl delivers for email, text messages, and calls, the HTTP headers that come with it, and how retries, duplicates, and replay work.
Every inbound email, text message, and call event is delivered to your endpoint in the same JSON envelope. Fields common to every channel sit at the top level. Channel-specific content is in payload.inline.
The envelope
{
"schema_version": "2026-03-23.v1",
"event_id": "evt_01j9x...",
"tenant_id": "t_acme...",
"binding_id": "b_support...",
"channel": "sms",
"provider": "telnyx",
"direction": "inbound",
"occurred_at": "2026-10-05T14:03:11.204Z",
"provider_event_type": "message.received.sms",
"provider_event_id": "b0c8...",
"dedupe_key": "b0c8...",
"subject": { "from": "+12125550183", "to": "+12125550142", "phone_number": "+12125550183" },
"payload": { "inline": { "text": "Can you check the delivery date?", "media_count": 0, "media": [] } },
"policy_result": { "decision": "allow", "reasons": [] },
"route_result": { "route_id": "r_...", "endpoint_ids": ["ep_..."], "dispatch_mode": "webhook" },
"delivery": { "status": "pending", "attempt": 1 },
"trace": { "trace_id": "...", "span_id": "...", "config_revision": 7 }
}| Field | Type | Description |
|---|---|---|
schema_version | string | Envelope version, currently 2026-03-23.v1. |
event_id | string | Unique event ID (evt_…). Use it to find the event in the dashboard, CLI, or API. |
tenant_id | string | Workspace ID (t_…). |
binding_id | string | The address or number that received the message (b_…). |
channel | email | sms | voice | MMS messages use sms. |
provider | smtp | cloudflare | telnyx | Where the message came from. Workspace-subdomain email uses smtp. |
direction | inbound | Always inbound. |
occurred_at | ISO 8601 | When the provider received the message or call event. |
provider_event_type | string | The specific event, for example email.inbound, message.received.mms, or call.recording.saved. See the channel pages. |
provider_event_id | string | The provider's ID for the message or callback. |
dedupe_key | string | Key AgentOwl uses to drop duplicate provider callbacks. |
subject | object | from, to, phone_number (SMS and voice), and call_session_id (voice). |
payload.inline | object | Channel-specific content. See Email, Text messages, and Voice. |
payload.payload_ref_id | string | Present instead of inline content for large messages. Fetch the content with the payload API. |
policy_result | object | decision (allow, deny, quarantine, or drop) and reasons. Only allowed events are delivered. |
route_result | object | The route that matched, its endpoint IDs, and dispatch_mode. |
delivery | object | status is pending while sending. attempt counts delivery attempts to this endpoint, starting at 1 and increasing on retries. Event records returned by the API retain an event-wide processing counter. |
trace | object | Tracing and config metadata, useful when contacting support. |
New fields may be added to the envelope and payloads at any time. Ignore fields you don't recognize.
HTTP request
Webhook endpoints receive one POST per event:
| Header | Value |
|---|---|
Content-Type | application/json |
X-AgentOwl-Event-Id | The event_id |
X-AgentOwl-Idempotency-Key | <event_id>:<endpoint_id>. The same on every retry of this event to this endpoint. |
X-AgentOwl-Timestamp | ISO 8601 time of this delivery attempt |
X-AgentOwl-Tenant-Id | The tenant_id |
X-AgentOwl-Signature | Versioned HMAC-SHA256 signature, when signing is enabled |
Authorization | Bearer <secret>, when the endpoint uses bearer authentication |
traceparent, tracestate | W3C trace context |
AgentOwl doesn't follow redirects. Use the final URL.
Responding
Return 2xx after verifying the request and durably accepting it, within the configured timeout (30 seconds by default). Slow agent execution belongs in a background worker. If storage fails, return 503; do not acknowledge work you have not saved. Endpoint configuration can override the timeout and accepted success codes.
Atomically insert a job with a unique X-AgentOwl-Idempotency-Key; acknowledge an already accepted duplicate without creating another job. Successful delivery records acceptance by your receiver, not completion by your agent.
Redirects are never followed. 3xx responses are terminal, even if a redirect code is listed in endpoint success codes. Use the final URL.
Retries and duplicates
When a retryable delivery fails, AgentOwl retries that endpoint up to 5 times by default, with increasing delay. The endpoint's max_retries setting controls retries after the initial attempt; 0 disables retries. Successful endpoints are skipped on subsequent attempts. When all endpoints have finished, the event is successful only if every endpoint succeeded; otherwise its terminal failure is recorded. Failed events appear in the dashboard under Webhooks → Failed and in agentowl logs tail.
A few things to plan for:
- You may receive an event more than once. If an endpoint accepts an event but its acknowledgement is lost, or the worker stops before saving the result, a retry can deliver it again. Store the idempotency key and skip deliveries you've already processed.
- Retryable webhook failures include
429,5xx, timeouts, and connection errors. Other HTTP failures, including other4xxresponses and any3xxresponse returned to the dispatcher, are terminal. Return2xxfor events you intentionally ignore. - Retries preserve the original route and endpoint configuration. Editing a route or URL does not redirect pending deliveries. Deactivating or deleting an endpoint stops its pending deliveries. Managed signing-key activation takes effect on pending retries without changing their destination. Explicit event replay creates a new event and currently evaluates current routing in both dispatch replay modes.
- Order isn't guaranteed. Use
occurred_atto order events, for example the callbacks of one call.
Verifying signatures
Newly activated signing uses v2: X-AgentOwl-Signature: v2=<lowercase hex digest>. The HMAC-SHA256 input is the exact X-AgentOwl-Timestamp header, a literal ., then the raw request body bytes. The timestamp is ISO 8601 (for example 2026-10-05T12:00:00.000Z).
The shared verifier is available in the source workspace. The standalone reference receiver verifies the same v2 format with Python standard-library functions and does not require a published SDK. Verify before parsing JSON:
import { verifyWebhookSignature } from '@agentowl/shared';
async function verify(request: Request, secret: string): Promise<boolean> {
return verifyWebhookSignature({
body: new Uint8Array(await request.arrayBuffer()),
timestamp: request.headers.get('X-AgentOwl-Timestamp') ?? '',
signature: request.headers.get('X-AgentOwl-Signature') ?? '',
secret,
});
}The verifier rejects malformed signatures, altered timestamps or bodies, and timestamps more than five minutes in the past or future. It does not accept legacy body-only signatures. Atomically store the idempotency key with durable acceptance to reject duplicate jobs within the allowed time window. Reading request.arrayBuffer() consumes the body; retain those bytes if you need to parse it afterward.
Enabling signing and rotating secrets
In Webhooks → Configuration, choose Signing beside an endpoint:
- Generate a signing secret and copy it immediately. It is shown only in the generation response.
- Save it in your receiver and configure v2 verification.
- Confirm your receiver is configured, then activate signing.
Generating a replacement leaves the active key unchanged. For rotation, configure the receiver to accept both old and new keys before activating the new one. Keep the old verification key briefly for requests already in flight (at least your verification tolerance plus request duration). New deliveries and pending retries use the activated key. If you lose a prepared secret, generate another; this invalidates the previous unactivated candidate.
The equivalent CLI flow is:
agentowl endpoint signing prepare ep_... --json
# Save the returned secret in your receiver, then use its key_id:
agentowl endpoint signing activate ep_... <key_id>The API operations are POST /v1/endpoints/{endpoint}/signing/prepare and POST /v1/endpoints/{endpoint}/signing/activate, with {"key_id":"..."} for activation. Both require tenant access and endpoints:write permission. Secrets are encrypted in the dedicated secret store and are never included in endpoint reads or the general idempotency-response cache. Retrying preparation generates a new candidate; activation of the same current key can safely be repeated.
Existing signed endpoints
Existing endpoints with no signature_version (or v1) keep their legacy body-only hex signature so deployed receivers continue to work. Their timestamp is not authenticated. Migrate by configuring v2 verification at the receiver and using the prepare/activate flow above. During a v1-to-v2 transition, explicitly support both formats only until activation and in-flight requests have drained, then disable v1. Do not automatically fall back to legacy verification when a v2 signature is invalid.
Self-hosted installations must configure SECRETS_STORE and PAYLOAD_ENCRYPTION_KEY before using the signing API; see local development.
Large payloads
SMTP messages with normalized content over 96 KiB use a private encrypted payload reference and a truncated Inbox preview to stay within queue limits. The payload API returns the complete normalized JSON, including the original body. Receivers must fetch referenced content before acting on it.
Where Cloudflare email payload externalization is used, email larger than 128 KB is stored separately instead of being sent inline. New hosted email enrolled after the October 16, 2026 release has a 30-day content window; earlier payloads and self-hosted defaults retain their existing expiry. The event has payload.payload_ref_id instead of payload.inline. Fetch the content with an API key:
curl -H "Authorization: Bearer $AGENTOWL_API_TOKEN" \
https://api.agentowl.dev/v1/events/evt_.../payloadAfter expiry this returns 410 with payload_unavailable or email_content_expired. Expired enrolled email also cannot be replayed.
Inspecting and replaying events
Look up an event by ID:
agentowl events inspect evt_01j9x...
# or: GET https://api.agentowl.dev/v1/events/evt_01j9x...Replay sends a stored event again, for example after you fix a bug in your webhook:
agentowl events replay evt_01j9x... --mode redrive-dispatch| Mode | What it does |
|---|---|
redrive-dispatch | Enqueues the stored event as new work; current implementation resolves current routes |
re-evaluate-route | Applies your current routes, then delivers |
simulate | Returns the stored event without enqueueing; does not evaluate current routes |
The consumer currently resolves current routing for both dispatch replay modes; it does not restore the original delivery plan for redrive-dispatch. Automatic retries retain their original plan.
Replays other than simulate create a new event with a new event_id and idempotency key, so your duplicate check won't drop them.
Other endpoint types
Besides webhooks, an endpoint can be:
| Type | Delivery |
|---|---|
agent-runtime | Same as webhook, for an agent's HTTP entrypoint |
queue | An authenticated HTTPS queue adapter (queue_adapter: "http"), Cloudflare Queue API (queue_adapter: "cloudflare"), or operator-allowlisted native producer (queue_binding). |
email-forward | Forwards the event using email_provider: "mailchannels" or an operator-configured "sendmailrs" SMTP relay. SMTP requires durable delivery guards. |
hosted-page | Stores the event on an authenticated, tenant-scoped page. Visibility expires after 30 days by default; hosted_page_ttl_seconds accepts 1–31536000 seconds. Retries do not extend expiry. |
sip | Transfers a live call to a SIP URI |
voice-runtime | Streams live call audio to your voice agent. See Voice. |
dead-letter | Keeps the event for inspection without delivering it |
These are created with the CLI or API; see agentowl endpoint create.
Content retention and access
| Content | Current scope |
|---|---|
| Event records | New hosted email content expires after 30 days from receipt following the October 16 rollout; operational event metadata remains separate |
| Externalized Cloudflare payloads | New enrolled hosted email: 30 days; historical/self-hosted default: seven days; unavailable content returns 410 |
| Managed SMTP/Cloudflare email attachments | Private managed storage with explicit expiry and authorized download; new hosted objects: 30 days; existing objects keep their recorded expiry |
| MMS and voice recording links | Carrier-hosted links with their own expiry; not covered by managed email archival |
Pro pricing's proposed retention is a launch assumption, not a guarantee for current event or media storage. Check email, SMS, voice, and managed attachments for the relevant path.
Email sender authentication describes origin evidence, not permission to execute instructions. SMTP workspace mail currently does not enforce sender allow/deny policies; see policy coverage. Configure your runtime's action authorization separately from delivery authentication.
Optional agent work
Endpoints with agent_work enabled receive an additive work block for runtime callbacks, conversation continuation, and authorized attachment retrieval. See Agent work and outcomes. Transport acknowledgement remains separate from the agent outcome.