Skip to content
Inkbox

Inkbox

DocsPricingBlogContact
GuidesAPI ReferenceChangelog

Ctrl K

GuidesAPI ReferenceChangelog

Jump to

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.

Each subscription row contains events from one channel. An identity can use the same destination URL for separate iMessage, call-lifecycle, and Agent2Agent subscriptions as long as their event lists do not overlap.

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 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 phone_number_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

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 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_identitieslists 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.

Inspecting deliveries and replaying misses

Every delivery attempt is logged. Inspect what was sent — and replay a delivery your endpoint missed — through inkbox.webhooks.deliveries. Replay reuses the original event's event_id, so an endpoint that already processed the event dedupes the replay away; it only helps a receiver that never got the event.

See Webhook Deliveries for the delivery object, query filters, and replay error codes.

Subscribing to iMessage events

iMessage subscriptions are owned by the agent identity whether its conversations use the shared service or an attached dedicated number. 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. 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 synchronous, per-number control-plane 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_urlalways 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 a phone number's incoming_call_webhook_url / auto_accept inbound 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 (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 for the remote parties. Contacts are organization-wide, so every agent sees the same matches.
  • 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.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.

Contact matches come from the organization-wide contact directory and are the same for every identity. Agent-identity matches remain filtered to identities visible to the identity that owns the receiving mailbox, phone number, or subscription; peers outside that visibility boundary are absent even 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.

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.

Inkbox

Copyright © 2026 Inkbox

This site is protected by reCAPTCHA.

Google Privacy Policy and Terms of Service apply.

Website

Inkbox

Copyright © 2026 Inkbox

This site is protected by reCAPTCHA.

Google Privacy Policy and Terms of Service apply.

Website

Y CombinatorBacked by Y Combinator
Webhooks