AgentOwl

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 }
}
FieldTypeDescription
schema_versionstringEnvelope version, currently 2026-03-23.v1.
event_idstringUnique event ID (evt_…). Use it to find the event in the dashboard, CLI, or API.
tenant_idstringWorkspace ID (t_…).
binding_idstringThe address or number that received the message (b_…).
channelemail | sms | voiceMMS messages use sms.
providersmtp | cloudflare | telnyxWhere the message came from. Workspace-subdomain email uses smtp.
directioninboundAlways inbound.
occurred_atISO 8601When the provider received the message or call event.
provider_event_typestringThe specific event, for example email.inbound, message.received.mms, or call.recording.saved. See the channel pages.
provider_event_idstringThe provider's ID for the message or callback.
dedupe_keystringKey AgentOwl uses to drop duplicate provider callbacks.
subjectobjectfrom, to, phone_number (SMS and voice), and call_session_id (voice).
payload.inlineobjectChannel-specific content. See Email, Text messages, and Voice.
payload.payload_ref_idstringPresent instead of inline content for large messages. Fetch the content with the payload API.
policy_resultobjectdecision (allow, deny, quarantine, or drop) and reasons. Only allowed events are delivered.
route_resultobjectThe route that matched, its endpoint IDs, and dispatch_mode.
deliveryobjectstatus 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.
traceobjectTracing 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:

HeaderValue
Content-Typeapplication/json
X-AgentOwl-Event-IdThe event_id
X-AgentOwl-Idempotency-Key<event_id>:<endpoint_id>. The same on every retry of this event to this endpoint.
X-AgentOwl-TimestampISO 8601 time of this delivery attempt
X-AgentOwl-Tenant-IdThe tenant_id
X-AgentOwl-SignatureVersioned HMAC-SHA256 signature, when signing is enabled
AuthorizationBearer <secret>, when the endpoint uses bearer authentication
traceparent, tracestateW3C 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 other 4xx responses and any 3xx response returned to the dispatcher, are terminal. Return 2xx for 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_at to 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:

  1. Generate a signing secret and copy it immediately. It is shown only in the generation response.
  2. Save it in your receiver and configure v2 verification.
  3. 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_.../payload

After 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
ModeWhat it does
redrive-dispatchEnqueues the stored event as new work; current implementation resolves current routes
re-evaluate-routeApplies your current routes, then delivers
simulateReturns 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:

TypeDelivery
agent-runtimeSame as webhook, for an agent's HTTP entrypoint
queueAn authenticated HTTPS queue adapter (queue_adapter: "http"), Cloudflare Queue API (queue_adapter: "cloudflare"), or operator-allowlisted native producer (queue_binding).
email-forwardForwards the event using email_provider: "mailchannels" or an operator-configured "sendmailrs" SMTP relay. SMTP requires durable delivery guards.
hosted-pageStores 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.
sipTransfers a live call to a SIP URI
voice-runtimeStreams live call audio to your voice agent. See Voice.
dead-letterKeeps the event for inspection without delivering it

These are created with the CLI or API; see agentowl endpoint create.

Content retention and access

ContentCurrent scope
Event recordsNew hosted email content expires after 30 days from receipt following the October 16 rollout; operational event metadata remains separate
Externalized Cloudflare payloadsNew enrolled hosted email: 30 days; historical/self-hosted default: seven days; unavailable content returns 410
Managed SMTP/Cloudflare email attachmentsPrivate managed storage with explicit expiry and authorized download; new hosted objects: 30 days; existing objects keep their recorded expiry
MMS and voice recording linksCarrier-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.

On this page