Skip to main content
Webhook delivery flows through a channel-agnostic subscription resource. Each subscription names one agent identity — one HTTPS destination URL, and a non-empty subset of the event catalog. Many subscriptions can attach to the same owner; each URL receives its own POST per event independently, so one slow receiver doesn’t block delivery to the others. Mix any of the 44 notification types in one subscription, even before the identity has the corresponding channels. Enabling a channel later starts matching events without a subscription edit. Selecting events does not provision resources, enable channels, or change billing. Multiple destination subscriptions remain supported; Select all selects the current event list, not future new event types. The one exception is phone.incoming_call. That event is a synchronous control-plane callback — the response body decides whether Inkbox answers — so it stays separate. Configure the identity’s incoming-call action. For ChatGPT event monitoring, ChatGPT manages a separate subscription through the Inkbox plugin. Existing webhook receivers keep working independently. ChatGPT can also receive an asynchronous incoming-call notification, which does not replace the synchronous call decision above.

Event reference

All 44 notification events and the separate incoming-call callback are listed below. Slack events cover content and conversations the identity can access.

One subscription across channels

Use SDK or CLI version 0.7.8 or later for mixed identity-owned subscriptions. The server must also advertise supports_identity_subscriptions: true in GET /webhooks/catalog. Before that capability is available, keep separate channel subscriptions using mailbox, phone, and identity selectors. Explicit identity-wide SDK lists report an unsupported-feature error rather than returning a partial list. Create a single receiver for any combination of notification types:
You can configure this before attaching a phone number or enabling iMessage or Agent2Agent. These selections remain configured as channels change. To receive every current notification, use the complete event list.

Subscribing to mail events

Pick a subset of message.* events to deliver to one URL. The full catalog is message.received, message.sent, message.forwarded, message.delivered, message.bounced, message.failed.
You can attach up to 60 active subscriptions per identity. Each URL receives its own POST per event, so split events across receivers or fan one URL out across many identities using separate subscription rows.

Phone webhooks

Identities have one synchronous incoming-call callback (incoming_call_webhook_url) for incoming calls, plus per-event subscriptions for the text lifecycle:
  • incoming_call_webhook_url — receives the flat, synchronous inbound-call payload (no envelope). Your response (action: "answer" | "reject" plus optional client_websocket_url) decides what happens to the call. Top-level contacts and agent_identities carry the matches for the caller.
  • Text events (text.received, text.sent, text.delivered, text.delivery_failed, text.delivery_unconfirmed) — subscribe to any subset via /webhooks/subscriptions with agent_identity_id. Standard envelope; data.contacts and data.agent_identities carry the matches for the sender or lifecycle recipient. The text_message body includes conversation_id, sender_phone_number, and outbound recipients[]; group lifecycle events also set top-level data.recipient_phone_number so receivers know which recipient changed state. Fire-and-forget — response status is logged but does not affect text processing.

Subscribing to text events

Use the same identity owner for text events. The text catalog is text.received, text.sent, text.delivered, text.delivery_failed, text.delivery_unconfirmed.
  • Mail events carry data.contacts — a list of {bucket, address, id, name} entries, one per matched recipient. The list is always present and sparse: unmatched recipients are absent, and "contacts": [] means nothing matched. Inbound mail resolves from_address plus every CC; outbound mail resolves every To, CC, and BCC. Pair entries back to recipients on (bucket, address) — the same address can appear in multiple buckets and will produce one entry per bucket. See Mail webhooks → Peer resolution for the full pairing rules, the intra-bucket dedupe behavior, and the per-event cap.
  • Text events carry data.contacts and data.agent_identities — lists of {id, name, memories} (contacts, where memories is string[]) or {id, agent_handle, display_name} (identities) entries. Lists are always present and possibly empty. Inbound and 1:1 events match the sender / counterparty; outbound group lifecycle events match per-recipient context, and data.recipient_phone_number plus data.text_message.recipients[] carry the per-leg state.
  • Inbound calls carry top-level contacts and agent_identities — same plural-list shape. Match key is remote_phone_number.
See Webhook Subscriptions for the full request and response shapes, validation rules, and error codes.

Per-message sender access

Received SMS/MMS, iMessage, and email events carry optional sender_access on the message itself: This is message admission, not permanent sender trust, explicit allowlist membership, or permission to execute commands. An applicable explicit block still suppresses delivery. Do not infer the value from companion.phase: a live event can contain either value. Rule changes do not rewrite a previously recorded message’s classification on retry. The same field appears on applicable inbound entries in inline companion.history and on every page of activation history. Preserve it when loading the complete snapshot. Python and Rust history entries expose sender_access; TypeScript exposes senderAccess. Raw webhook JSON uses sender_access in all SDKs. The only values are direct and sponsored. Absence means unknown or inapplicable: older payloads without receipt evidence, outbound history/lifecycle events, reactions, and system-generated email notices (such as delivery-failure and storage notifications) may omit it. Never treat absence as direct. Upgrading a receiver requires a compatible SDK parser; unknown fields are not necessarily retained by older SDKs. These are abbreviated payload fragments, not complete events:
Direct admission
Sponsored admission

Companion conversation events

Companion mode adds an optional top-level companion object to message.received, text.received, and imessage.received. The original channel message stays in data. Existing event types, subscriptions, and signature verification still apply. SDK support requires 0.7.3 or later. This object is separate from optional data.context. Disabling recent-message context does not disable Companion initialization. Do not substitute contact-wide history for an activation snapshot. With Companion mode enabled, the first qualifying authenticated group message from a sender with active exact inbound and outbound allows activates the scope. That actual author is its sponsor. Other eligible senders’ later messages in the same active scope are live events, not new initializations. An old activation never silently switches sponsors after revocation. See eligibility and activation. Python and TypeScript MailWebhookPayload, TextWebhookPayload, and IMessageWebhookPayload expose optional companion metadata. TypeScript webhook fields retain the wire’s snake_case names, unlike camelCase resource results. Rust provides inkbox::webhooks::CompanionMailWebhookPayload, CompanionTextWebhookPayload, and CompanionIMessageWebhookPayload through inkbox::companion::WithCompanion<T>; legacy payload types keep their existing shapes.

Delivery phases

channel is mail, phone, or imessage; phone denotes text messages here. sequence is a positive authorized-delivery sequence within the scope, not an index into all historical messages. Later permitted messages in an active scope use live, including messages from an ordinarily allowed sponsor. This example shows only the companion portion of a received event. The event’s stable id, event type, timestamp, and original data are still present in the full envelope.
JSON
history entries and reply_context use the activation-history shapes. Inline history is bounded and may not contain the trigger yet. The full snapshot includes it exactly once. The webhook cursor is history_next_cursor; the history API’s continuation field is next_cursor. Treat both as opaque. The SDK initialization helpers start from the first API page and validate the entire snapshot rather than trusting a partial inline page.

Receiver ordering and recovery

Verify signatures before trusting metadata. Deduplicate the stable event id, durably reserve the activation, and buffer later live events while loading all snapshot pages. Then submit one combined host input containing history and trigger, persist the initialization checkpoint and host turn binding, and release live events in sequence. Historical text must not enter command or approval parsers. Do not send the trigger to the host again as an unrelated live message. The first observed event for an activation can be live. Recover initialization through the history API before handling it. Ordinary events without an activation use ordinary permissions and a separate conversation-scoped session; they must not fetch a nonexistent activation or wait for one. Retries retain stable event and activation IDs. Delivery is at least once. A successful webhook HTTP response does not prove the agent finished initialization, and does not guarantee exactly-once agent execution. Reconcile an uncertain host submission before retrying it. Follow the receiver checklist and SDK examples. Manual delivery-log replay returns 409 for Companion events. Recover through currently authorized conversation state and activation history; automatic webhook retries still apply. A history read can fail after revocation, so do not reuse previously loaded context as proof of current permission.

Inspecting deliveries and replaying misses

Delivery attempts are logged. For replayable channels, inspect what was sent and replay a delivery your endpoint missed through inkbox.webhooks.deliveries. Slack diagnostics contain event coordinates without original message content, and Slack replay returns 422. Replay preserves the webhook envelope’s id, so use that field to deduplicate events. The delivery record exposes the same value as event_id. Companion events use snapshot recovery instead of manual delivery-log replay. The replay examples below apply to replayable events.
See Webhook Deliveries for the delivery object, query filters, and replay error codes.

Subscribing to Slack events

Use agent_identity_id with a subset of the 23 Slack event types, alone or mixed with other notification types. Choose DMs, group DMs, channel messages, mentions, and thread replies directly through event_types. There is no separate filter configuration. Events cover all accessible conversations across the identity’s connected workspaces. Use scope=identity when updating or deleting a mixed subscription. One incoming message produces at most one delivery per subscription, even if several selected types match. The delivered type is a selected matching type, with priority mention → thread reply → DM → group DM → channel message. Deduplicate retries by the stable event id. Slack events ignore context_config, including when their subscription also selects received email, text, or iMessage events. Your runtime owns wake rules, thread watches, and memory; fetch live or retained Slack history explicitly when needed. slack.thread_reply_received means any reply, not a remembered watch. Edits and deletions have their own event types and are not restricted by the incoming types you select. See Slack webhooks for a registration example, payload fields, event selection, and interaction handling.

Subscribing to iMessage events

iMessage subscriptions are owned by the agent identity whether its conversations use shared service or an attached dedicated line. The catalog is imessage.received and imessage.reaction_received for inbound traffic, plus imessage.sent, imessage.delivered, and imessage.delivery_failed for outbound delivery status. On dedicated lines, imessage.received can carry a non-phone sender address in data.message.remote_number. Preserve the address as returned and read the message normally. These conversations are receive-only, so do not automatically reply to every received event.
Standard envelope; data.message is populated on imessage.received and the delivery-lifecycle events, and data.reaction on imessage.reaction_received. Fan-out pauses while the identity is paused or not iMessage-enabled, and contact-rule-blocked traffic never emits events. See iMessage webhooks for payload shapes. Received events (message.received, text.received, imessage.received) can also carry an additive data.context block when the subscription configures conversation context.

Subscribing to call-lifecycle events

Call-lifecycle subscriptions are owned by the agent identity, like iMessage — a call may ride the identity’s shared iMessage line rather than a number you own, and the identity is the stable owner either way. The catalog is call.ended, delivered once after a connected call terminates (Inkbox Voice AI calls, inbound and placed alike, also report when they never connect — see when it fires). Unlike phone.incoming_call — the separate synchronous incoming-call callback whose response body decides whether Inkbox answers — call.ended is a fire-and-forget, replayable fan-out. You receive it even if you never held the live media WebSocket, the response status is logged but never affects call processing, and the stable event id (evt_...) is your idempotency key across the original delivery and any replays.
Standard signed envelope. data.call is the terminated call (webhook wire shape, so no is_blocked) with a derived duration_seconds; data.contacts and data.agent_identities resolve the remote party. Two transcript fields ride the payload:
  • data.transcript — an inline, abridged (middle-cut) transcript block, present when the call has transcribed turns at dispatch time; it is null otherwise. Its entries mix {party, text, ts_ms} turns with an {marker: "abridged", omitted_turns, omitted_ms} marker where turns were dropped to fit the size budget, and abridged is true when anything was omitted. A signing-secret-only consumer can act on this copy without an API key.
  • data.transcript_url — always present, and points at GET /phone/calls/{id}/transcripts, the authoritative verbatim record. Fetching it requires an API key (see Transcripts for scoping). The inline block reads transcripts as of dispatch time and may lag the very last turn, so transcript_url is the source of truth when you need the complete transcript.
This subscription is an independent row from the identity’s incoming-call routing — the two never interfere. For Inkbox Voice AI calls the payload additionally carries an outcome and the post_call_action_items the agent recorded — the event is the one atomic delivery of the whole post-call package. See Phone webhooks for the full payload shape.

Incoming-call webhooks (separate control)

phone.incoming_call uses the identity’s separate incoming-call action. The receiver’s response body controls whether Inkbox answers, rejects, or ignores the call, so this callback does not fan out.

Peer resolution

Mail, text, iMessage, and call webhook payloads carry two parallel lookups for the remote parties on the event:
  • contacts — address-book matches visible to the receiving identity. Profile controls whether a match is included; Memories can further hide memory text. Hidden matches are omitted.
  • agent_identities — active internal-agent matches in the same organization.
Both lists are always present and possibly empty, never null. A single peer can land in both — receivers decide precedence per row. The shape differs by surface:
  • Mail events carry data.contacts and data.agent_identities, each a list of {bucket, address, id, ...} entries — one per matched recipient. Pair entries back to their recipient slot on (bucket, address) since the same address can appear in multiple buckets. See Mail webhooks → Peer resolution for the full pairing rules.
  • Text events carry data.contacts and data.agent_identities keyed off the remote party. Each entry is {id, name, memories} (contacts, where memories is string[]) or {id, agent_handle, display_name?} (identities).
  • Inbound calls carry top-level contacts and agent_identities with the same per-entry shape as text events.
  • iMessage events carry data.contacts and data.agent_identities keyed off the 1:1 counterparty or the sender of an inbound group message, with the same per-entry shape as text events. Group delivery events have no single counterparty, so both lists may be empty.
  • Call-ended events carry data.contacts and data.agent_identities keyed off data.call.remote_phone_number, with the same per-entry shape as text events.
Slack payloads do not include these peer lookups. They carry event-local workspace and conversation coordinates instead. Contact matches follow the receiving identity’s permissions. A permitted name can accompany an empty memory list, or explicitly permitted memories can accompany a hidden name. Ordinary webhooks remain scoped even when the identity’s voice default is YOLO. Agent-identity matches include active identities in the same organization when the email or phone matches.

Signing keys

Signing keys are per agent identity: each identity has its own key that verifies the webhooks (and WebSocket upgrades) for that identity’s mailbox, phone number, iMessage, and call-lifecycle traffic. The first webhook subscription you create for a keyless identity returns its signing secret once, in the create response. See the Signing Keys page for details on creating and rotating keys.

Authenticating to your endpoint

Some receivers sit behind standard bearer authentication — an automation platform’s inbound webhook, or your own API middleware that rejects any request without an Authorization header before your handler runs. For those endpoints, set an optional auth_token on the subscription. When it’s set, every delivery for that subscription — live events on any channel (mail, text, iMessage, call-lifecycle, Agent2Agent, Slack) and replays alike — carries Authorization: Bearer <token>:
bash
The stored token can be read back: subscription reads return it as auth_token (null when unset) alongside a has_auth_token boolean. On PATCH it is tri-state, exactly like context_config: send a string to replace the stored token, null to clear it, or omit the field to leave it unchanged. Once a token is configured, every delivery for that subscription carries the Authorization header.
The bearer token and the signature headers are complementary, not alternatives. The auth_token authenticates Inkbox to your endpoint and is per-subscription; the X-Inkbox-* signature headers prove payload integrity and are per-identity. Setting a token changes nothing about signing — the three signature headers are still sent whenever the owning identity has a signing key — so keep verifying signatures even on token-protected endpoints. The token rides webhook deliveries only. The separate phone.incoming_call callback and the phone media-stream WebSocket upgrade are not subscription deliveries and do not carry it.

Verifying webhook signatures

Use verify_webhook / verifyWebhook to confirm that an incoming request was sent by Inkbox. Pass the plaintext key from your signing key as the secret.

Receiving webhooks (typed)

The SDK exports typed payload shapes for every webhook body. Pair verify_webhook / verifyWebhook with a single cast(...) or as ... and discriminate on event_type.

Mail handler

Mail events carry data.contacts and data.agent_identities as lists. Pair each entry to its recipient field on (bucket, address) — the same address can match in multiple buckets and will appear once per bucket per list.

Text handler

Text events carry data.contacts and data.agent_identities as lists (always present, possibly empty). In group lifecycle events, data.recipient_phone_number names the recipient this webhook is about, while data.text_message.recipients[] carries every recipient’s current delivery state.

Call handler

Inbound-call events carry top-level contacts and agent_identities (the call payload is flat — no envelope).

Call-lifecycle handler

call.ended carries a standard envelope. Discriminate on event_type, then act on the inline data.transcript when it’s present, or fetch data.transcript_url for the verbatim record. The response body is ignored.
Wire shapes are intentionally snake_case — they mirror the raw JSON body, not the SDK’s parsed (camelCase in TypeScript) response types — so JSON.parse(body) as MailWebhookPayload and cast(MailWebhookPayload, json.loads(body)) round-trip without a transformer. Enum-valued fields like direction, status, and delivery_status are string-literal unions rather than the SDK’s StrEnum / TS enum exports, because json.loads / JSON.parse produce bare strings and string-literal unions narrow cleanly under mypy / pyright / tsc. The mail-side per-recipient entry is exposed as WebhookMailContact ({ bucket, address, id, name }) and the bucket enum as MailContactBucket ("from" | "to" | "cc" | "bcc"), available alongside MailWebhookPayload. Text and inbound-call events expose plural lists: WebhookContact[] ({ id, name, memories }, where memories is string[]) and WebhookAgentIdentity[] ({ id, agent_handle, display_name }). Text message payloads additionally expose outbound recipients[] entries and top-level recipient_phone_number for group lifecycle fan-out. The call.ended payload is exposed as CallEndedWebhookPayload, with WebhookPhoneCall for data.call, WebhookCallTranscript for the inline data.transcript block, and WebhookPostCallActionItem (Python: WebhookPostCallActionItemWire) for data.post_call_action_items entries; the Voice AI additions (mode, reason, outcome, post_call_action_items) are optional-with-defaults so payloads from before SDK 0.4.22 parse unchanged.