Webhooks
Webhook delivery flows through a channel-agnostic subscription resource. Each subscription names one owner — a mailbox, a phone number, or an 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.
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 on a per-number field. Configure it via incoming_call_webhook_url on the phone number resource.
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 20 active subscriptions per mailbox. Each URL receives its own POST per event, so split events across receivers or fan one URL out across many mailboxes from many subscription rows.
Phone webhooks
Phone numbers have one synchronous control-plane 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 optionalclient_websocket_url) decides what happens to the call. Top-levelcontactsandagent_identitiescarry 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/subscriptionswithphone_number_id. Standard envelope;data.contactsanddata.agent_identitiescarry the matches for the sender or lifecycle recipient. Thetext_messagebody includesconversation_id,sender_phone_number, and outboundrecipients[]; group lifecycle events also set top-leveldata.recipient_phone_numberso receivers know which recipient changed state. Fire-and-forget — response status is logged but does not affect text processing.
Subscribing to text events
Same pattern with phoneNumberId. 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 resolvesfrom_addressplus 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.contactsanddata.agent_identities— lists of{id, name}(contacts) 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, anddata.recipient_phone_numberplusdata.text_message.recipients[]carry the per-leg state. - Inbound calls carry top-level
contactsandagent_identities— same plural-list shape. Match key isremote_phone_number.
See Webhook Subscriptions for the full request and response shapes, validation rules, and error codes.
Subscribing to iMessage events
iMessage subscriptions are owned by the agent identity — iMessage conversations ride shared lines rather than a number you own, so the identity is the stable owner. 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.
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.
Incoming-call webhooks (still per-number)
phone.incoming_call is the only event that lives on the phone-number resource, because the receiver's response body controls whether Inkbox answers, rejects, or ignores the call. Fan-out makes no sense here.
Peer resolution
Every webhook payload carries two parallel lookups for the remote parties on the event:
contacts— address-book matches gated by Contact access.agent_identities— internal-agent matches gated by Agent visibility, with self-visibility (agents always match themselves).
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.contactsanddata.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.contactsanddata.agent_identitieskeyed off the remote party. Each entry is{id, name}(contacts) or{id, agent_handle, display_name?}(identities). - Inbound calls carry top-level
contactsandagent_identitieswith the same per-entry shape as text events. - iMessage events carry
data.contactsanddata.agent_identitieskeyed off the connected human's number, with the same per-entry shape as text events.
Resolution is scoped to the identity that owns the receiving mailbox or phone number — or, for iMessage, the identity that owns the subscription — peers not visible to that identity are absent even when the email or phone matches.
Signing keys
See the Signing Keys page for details on creating and rotating keys.
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).
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 }) 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.