text.* events you want. The call-lifecycle event (call.ended) is delivered the same way, via a subscription on the agent identity. The incoming-call event stays a per-number control-plane callback, configured on the phone number resource via incoming_call_webhook_url.
Event types
phone.incoming_call is not subscribable. Its handling is synchronous (your response decides whether Inkbox answers the call), so it can’t fan out. Configure it via incoming_call_webhook_url on the phone number resource.
call.ended is owned by the agent identity rather than a phone number — 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. Subscribe with agent_identity_id.
Inbound texts rejected by a contact-rule block or by default-block in whitelist mode are retained for admin review but do not emit text.received webhooks.
Configuring incoming-call webhooks
incoming_call_action to "webhook" and provide an incoming_call_webhook_url on the phone number.
This per-number callback does not support a delivery auth_token — that option exists only on webhook subscriptions.
Request body
Request example
JSON
Code examples
Configuring text webhooks
Text events are delivered via the Webhook Subscriptions API. Two steps:- Have a phone number provisioned (see Phone numbers).
- Create a subscription naming that phone number, the HTTPS destination URL, and the subset of
text.*event types you want delivered.
Request example
JSON
Code examples
Configuring call-lifecycle webhooks
call.ended is delivered via the same Webhook Subscriptions API, but the subscription is owned by the agent identity — pass agent_identity_id instead of phone_number_id. A subscription carries one channel, so call.ended cannot share a row with imessage.* events; create a second subscription on the same identity if you want both.
Request example
JSON
Incoming call webhook
When aphone.incoming_call event fires, Inkbox sends a signed POST request to the configured webhook URL. The payload is flat — there is no {event_type, timestamp, data} envelope, and contacts + agent_identities sit at the top level alongside the call fields. Your endpoint must respond with an action instructing Inkbox how to handle the call.
Payload shape
JSON
Payload fields
When
origin is "shared_imessage_number", the inbound call arrived on the identity’s shared iMessage line and local_phone_number is null. A dedicated iMessage line uses "dedicated_imessage_number" and surfaces that line as local_phone_number.
contacts and agent_identities are always present on the wire and possibly empty. A peer that’s both a contact and an internal agent appears once in each list.
Blocked calls do not dispatch webhooks — receivers only ever see calls that survived the inbound filter.
Response
Your endpoint must respond with an action instructing Inkbox how to handle the call.client_websocket_url is optional — omit it (or send null) to fall back to the phone number’s configured client_websocket_url.
Override the fallback by supplying a per-call URL:
JSON
JSON
Response fields
Call-ended webhook
call.ended fires once per call, after a connected call terminates. Calls that never connect (unanswered, rejected, or failed before pickup) don’t emit the event — with one exception: Inkbox Voice AI calls report every terminal state, inbound and placed alike, so the event arrives even when the call never connected, with outcome saying what happened. A Voice AI call is never silent. The payload uses the standard {event_type, timestamp, data} envelope, plus a stable top-level id (evt_...) that survives retries and replays — use it as your idempotency key.
Payload shape
JSON
Envelope fields
data.call fields
The webhook wire shape of the call — the call object minus is_blocked (blocked calls never dispatch lifecycle webhooks) and the connection-config fields, plus a derived duration_seconds. Post-call action items don’t ride inside data.call — they arrive at data.post_call_action_items (below).
data fields
outcome values
outcome describes how an Inkbox Voice AI call turned out; it is null whenever data.call.mode is "client_websocket".
post_call_action_items items
Ordered by seq, ascending. Items the agent canceled during the call are omitted from the payload. The call.ended event delivers the items atomically with the transcript and outcome — and like every webhook it carries a stable ID and is replayable, so a missed delivery can be re-fetched. The same open items are also readable off the call object (post_call_action_items) at any time.
transcript block
entries is an ordered mix of transcript turns and, when the transcript was cut to fit the size budget, a single middle-cut marker:
The inline block reads transcripts as of dispatch time and may lag the very last turn — treat
transcript_url as the source of truth when you need the complete transcript.
mode, hosted_agent_authority_mode, voicemail_detection, reason, outcome, and post_call_action_items are additive — consumers written before they existed keep parsing unchanged.
Text webhooks
All fivetext.* events share the standard {event_type, timestamp, data} envelope. data.text_message carries the stored text-message record. Two parallel peer-resolution lists ride alongside it:
All five events share the standard {event_type, timestamp, data} envelope. data.text_message carries the stored text-message record. For 1:1 traffic, the legacy remote_phone_number field stays populated. For outbound group lifecycle events, remote_phone_number is null; use data.recipient_phone_number for the recipient this event is about and data.text_message.recipients[] for the full per-recipient state.
On text.received, a subscription that configures conversation context also receives an additive data.context block; it is absent otherwise.
Inbound — text.received
JSON
sender_phone_number carries the remote sender (same value as remote_phone_number); recipients is null; data.recipient_phone_number is null.
The four outbound events share the same envelope and message shape as text.received. The headline value of these events is the delivery-state block. For 1:1 messages, delivery_status, error_code, error_detail, sent_at, delivered_at, and failed_at are mirrored on text_message; for group messages, delivery_status is a message-level rollup while per-recipient lifecycle details live in text_message.recipients[].
For groups, the top-level text_message.delivery_status is a rollup across recipients[] — the lowest lifecycle rank present, with "delivered" preferred at the terminal tier on mixed-outcome ties. Outbound events fan out one webhook per per-recipient transition, so a text.delivered event for one leg can fire while other legs are still "sent" — the rollup will still read "sent" until every leg reaches a terminal state. Treat recipients[].delivery_status as authoritative per-leg; use the rollup only for at-a-glance UI.
The four outbound events share the same envelope and message shape as text.received. The headline value is the delivery-state block — delivery_status, error_code, error_detail, sent_at, delivered_at, failed_at — which lets receivers act on outbound failures without a follow-up API call. On 1:1 outbound rows, those fields are hoisted out of recipients[0] for back-compat.
Example: text.delivery_failed on a 1:1 outbound send.
JSON
text.delivered for one recipient.
JSON
text.sent— carrier accepted the message.delivery_statusis"sent",sent_atis populated,delivered_atandfailed_atarenull.text.delivered— carrier confirmed handset delivery.delivery_statusis"delivered", bothsent_atanddelivered_atare populated.text.delivery_unconfirmed— carrier could not confirm delivery within its reporting window.delivery_statusis"delivery_unconfirmed",sent_atandfailed_atare populated,delivered_atisnull.
Outbound group lifecycle (group MMS)
When you send a group MMS (2+ recipients), each lifecycle event fires once per recipient, withdata.recipient_phone_number identifying which leg the event is about. The text_message row carries remote_phone_number: null (no single remote party) and a top-level delivery_status rollup; legacy top-level timestamp/error fields stay null, and per-leg state lives in recipients[].
Example: text.delivered for one leg of a 3-recipient group send.
JSON
text_message.direction === "outbound" && text_message.remote_phone_number === null, then read data.recipient_phone_number to identify the leg and find the matching entry in text_message.recipients[] for that leg’s delivery state.
Per-recipient × per-subscription fan-out
A 3-recipient group send with 2 active text subscriptions produces 6 POSTs per lifecycle stage — one per recipient leg, per subscription. Each POST carries the full group state intext_message.recipients[] plus the leg-identifying data.recipient_phone_number. Per-URL POSTs are independent: a slow receiver doesn’t block delivery to the others. See Phone texts for the API surface around sending into a conversation_id or addressing a group send.
text_message field reference
recipients[i] shape
Conversation context
When a subscription setscontext_config, text.received payloads gain an additive data.context object keyed by the configured classes (email, texts, calls). The texts class is conversation-scoped (the triggering conversation, with SMS and iMessage merged); email and calls resolve contact-scoped history through the matched contact. Delivery-status events never carry context.
The block shape — per-class fields, item shapes, transcript abridgment, skip reasons, and the 256 KB cap — is documented on the Mail webhooks page, and the configuration shape on the Webhook Subscriptions page.
Peer resolution
Inbound-call payloads carry top-levelcontacts and agent_identities. Text and call-ended payloads carry data.contacts and data.agent_identities. Both are lists — always present and possibly empty — never null.
- Field shapes: contacts are
{ "id": "<uuid>", "name": "<preferred name> | null", "memories": ["<active memory>"] }; agent identities are{ "id": "<uuid>", "agent_handle": "<handle>", "display_name": "<name or null>" }. Contact memories are newest first and may be absent on older replayed payloads.nameisnullwhen the contact has no name on file — a contact created automatically from an inbound call or text has anidandmemoriesbefore anyone gives it a name. It never falls back to the phone number, so check it before addressing someone by name. - Scope: contact matches follow the receiving identity’s permissions. Profile controls whether a match is included; Memories can further hide memory text. Ordinary webhook enrichment stays scoped even when a hosted call uses YOLO. Agent-identity matches include active identities in the same organization.
- Inbound calls: match key is top-level
remote_phone_number. - Call-ended events: match key is
data.call.remote_phone_number. - Inbound texts: match key is
data.text_message.remote_phone_number, also available asdata.text_message.sender_phone_number. - Outbound 1:1 text lifecycle events: match key is
data.text_message.remote_phone_number;data.recipient_phone_numberisnull. - Outbound group text lifecycle events: match key is
data.recipient_phone_number, the recipient this lifecycle event is about. Usedata.text_message.recipients[]to inspect every recipient in the conversation send. - Tiebreak (contacts): if multiple contacts share the same phone, the oldest by
created_atwins. Receivers needing disambiguation can callGET /contacts/lookup. - Hydration: feed
contacts[i].idintoGET /api/v1/contacts/{contact_id}. Agent-scoped credentials can discover peer identities throughGET /api/v1/identities/a2a/directory;GET /api/v1/identities/{agent_handle}resolves only the caller’s own identity for those credentials.
Verifying webhook signatures
Inkbox signs every webhook payload with the agent identity’s signing key — specifically, the key for the identity that owns the phone number the subscription (or incoming-call callback) is on, or, for call-lifecycle subscriptions, the identity that owns the subscription itself. Create or rotate that key via the Signing Keys guide. Three signature headers are included with each request:
When a subscription sets an
auth_token, its deliveries additionally carry Authorization: Bearer <token> — the bearer token authenticates Inkbox to your endpoint, while the signature headers prove the payload. The per-number phone.incoming_call callback does not support auth_token. See Authenticating to your endpoint.
The signature input is: {request_id}.{timestamp}.{raw_body}
To verify a webhook:
- Check the timestamp is within 300 seconds of the current time
- Reconstruct the signed message:
{X-Inkbox-Request-ID}.{X-Inkbox-Timestamp}.{raw_body} - Compute the HMAC-SHA256 using the agent identity’s signing key
- Compare the hex digest with the
sha256=...value from the header
Python verification example
Python
Disabling webhooks
To stop receiving text or call-lifecycle webhooks at a URL, delete the subscription that owns it. Delivery stops immediately. To stop receiving incoming-call webhooks, update the phone number to clear the URL and switch the action:JSON

