Skip to main content
Webhooks let you receive HTTP POST callbacks for mailbox events. Configure delivery by creating one or more webhook subscriptions on the agent identity — each subscription names one HTTPS URL and the subset of message.* events you want delivered there. When an event fires, Inkbox sends a signed POST request to every subscription URL whose event_types list includes that event. The request includes headers you can use to verify the payload’s authenticity. message.received can include optional top-level Companion metadata. Handle its ordinary, initialization, and live phases before sender-based routing. An activation requires complete snapshot pagination and one combined initialization input; optional recent-message context is not a replacement. Received events include optional data.message.sender_access: "direct" when the message passed contact rules at receipt (including allowed by default), or "sponsored" when conversation sponsorship permits an originally default-filtered message. This is not sender trust or command permission. Unknown, system-generated, and outbound cases omit it. See per-message sender access.

Event types


Payload shape

Every event uses the same envelope. Its id stays the same across retries and replays; use it to deduplicate events. data.message carries the message payload, including the owning mailbox’s email_address. Two peer-resolution lists accompany it:
  • data.contacts — address-book matches.
  • data.agent_identities — internal-agent matches (other agents in your organization that match a recipient).
On message.received, a subscription that configures conversation context also receives an additive data.context block; it is absent otherwise. Both lists are always present and possibly empty, never null. Each entry is tagged with the bucket (from / to / cc / bcc) it came from. A peer that’s both a contact and an internal agent appears once in each list. See Peer resolution below for the full pairing rules.

Inbound example — message.received

JSON

Outbound example — message.sent

JSON
Both data.contacts and data.agent_identities are always present on the wire and sparse — only matched recipients appear. An empty list means nothing matched. The same recipient can appear in both lists (when a peer is both a contact and an internal agent in your org); receivers decide precedence per row. data.message.bcc_addresses is symmetric with to_addresses and cc_addresses and is populated on outbound events only. Inbound payloads always carry "bcc_addresses": null, since BCC headers are not visible to recipients.

Message body

Inbound message.received events include a plain-text body when one is available. The body is limited to 16 KiB of UTF-8 text, with truncation at a complete character boundary. These five fields are present as null on outbound and delivery-status events. A delivery notice received as message.received may also have all five fields set to null. For inbound mail without usable text, body_state is unavailable, body_truncated is false, and the body and character counts are null. When body_state is truncated or unavailable, get the message using data.message.email_address and data.message.id to retrieve its stored text and HTML parts.

Peer resolution

data.contacts and data.agent_identities are parallel lists of address-book and internal-agent matches respectively — one entry per matched recipient per list. Each entry is self-describing about which recipient bucket it pairs back to.
  • contacts entry shape: { "bucket": "from" | "to" | "cc" | "bcc", "address": <wire-form recipient>, "id": <uuid>, "name": <preferred name> | null }. name is null when the contact has no name on file. An automatically created contact uses the sender’s display name when provided, but never falls back to the email address, so check the field before addressing someone by name.
  • agent_identities entry shape: { "bucket": "from" | "to" | "cc" | "bcc", "address": <wire-form recipient>, "id": <uuid>, "agent_handle": <handle>, "display_name": <preferred name> | null }.
  • Match coverage per direction:
    • Inbound (message.received) — from_address plus every entry of cc_addresses.
    • Outbound (message.sent, message.delivered, message.forwarded, message.bounced, message.failed) — every entry of to_addresses, cc_addresses, and bcc_addresses.
  • Scope. Contact matches follow the receiving identity’s permissions. Profile controls whether a match is included; Memories can further hide memory text. Agent-identity matches include active identities in the same organization.
  • Pair on (bucket, address), not on address alone. The same address can appear in multiple buckets on a single send (e.g. the same recipient in both to_addresses and cc_addresses). When that happens, the matched entry appears twice in the list — once per bucket. Pairing on address alone produces phantom duplicates or attributes the match to the wrong recipient slot.
  • Intra-bucket dedupe. Duplicate addresses inside the same bucket collapse to a single entry. Case-only intra-bucket duplicates also collapse, with the first occurrence’s wire form preserved in address.
  • Address casing. address echoes the original wire form of the matched recipient — the same casing that appears in the corresponding data.message.{from_address|to_addresses|cc_addresses|bcc_addresses} field. Resolution itself runs against the lowercased canonical form, so receivers parsing arbitrary inbound mail may want to use a case-insensitive compare when pairing.
  • Sparse lists. Only matched recipients appear — there is no placeholder entry for unmatched recipients. "contacts": [] and "agent_identities": [] each mean nothing matched in that list.
  • Wire ordering. from → to → cc → bcc, and within each bucket the order matches the source field’s order (first occurrence wins on intra-bucket dedupe).
  • Per-event cap. Up to 50 distinct normalized addresses are resolved per event. Over-cap inputs and transient resolver failures fall back to empty lists; the message webhook itself still fires unchanged.
  • Tiebreak. If multiple contacts share the same email, the oldest by created_at wins. Receivers needing disambiguation can call GET /contacts/lookup. Agent-identity matches are 1:1 on the canonical email.
  • Hydration. Feed contacts[i].id into GET /api/v1/contacts/{contact_id} to fetch the full contact record — see Manage contacts. Agent-scoped credentials can discover peer identities through GET /api/v1/identities/a2a/directory; GET /api/v1/identities/{agent_handle} resolves only the caller’s own identity for those credentials.
Treat empty lists as “no matches,” never as errors.

Conversation context

When a subscription sets context_config, message.received payloads gain an additive data.context object keyed by the configured classes (email, texts, calls), so a handler can act on recent history without a follow-up API call. Delivery-status events never carry it. Each class is a block: Item shapes by class:
  • email — id, direction, from_address, to_addresses, subject, snippet, created_at (metadata and snippet only; fetch full bodies via the messages API).
  • texts — id, channel (sms, imessage, or whatsapp, depending on the trigger), direction, sender, text (capped at 4,096 chars, with text_truncated), status, media, created_at.
  • calls — call_id, direction, remote_number, duration, started_at, abridged, and transcript. Each transcript entry is a turn (party, text, ts_ms, optional nullable phone_number) or, past 12,000 chars, an abridgment marker {"marker": "abridged", "omitted_turns": N, "omitted_ms": M} replacing the omitted middle — discriminate on marker. ts_ms is a capture timestamp in Unix epoch milliseconds; transcript line attribution explains phone_number.
For a whatsapp.received trigger from a non-group conversation, the texts block merges channel: "whatsapp" items from the triggering conversation with SMS and iMessage for the same remote number. A group whatsapp.received trigger includes only that group’s WhatsApp history, without SMS or iMessage. Mail, SMS, and iMessage triggers do not add WhatsApp history to their texts context. Nullable item-level fields are present as null, not omitted. Within call transcript turns, phone_number may be absent on older payloads or set to null; treat both as unavailable attribution. The whole block is capped at 256 KB; over the cap, the oldest items are dropped class by class (at least one per configured class survives) and that class’s truncated is set.
JSON
The full configuration shape and bounds live on the Webhook Subscriptions page.

Configuring webhooks

Mail webhook delivery is configured via the Webhook Subscriptions API. Two steps:
  1. Choose an agent identity. You can subscribe before its mail channel is available.
  2. Create a subscription naming that identity, the HTTPS destination URL, and the subset of message.* event types you want delivered.
You can attach up to 60 active subscriptions per identity — split events across URLs, or fan one URL out across many identities from many subscription rows. Creating a 61st returns 409; delete an existing one first.

Request example

JSON

Code examples

See Webhook Subscriptions for the full CRUD surface, validation rules, and per-route error codes.

Verifying webhook signatures

Inkbox signs every webhook payload with the agent identity’s signing key. Use the key of the identity that owns the subscription. Create or rotate a key via the Signing Keys guide. Each webhook request includes three signature headers: When the subscription sets an auth_token, each request additionally carries Authorization: Bearer <token> — the bearer token authenticates Inkbox to your endpoint, while the signature headers prove the payload. See Authenticating to your endpoint. The signature input is constructed as:

Verification steps

  1. Check the timestamp — reject the request if X-Inkbox-Timestamp is more than 300 seconds from the current time.
  2. Reconstruct the message — concatenate {X-Inkbox-Request-ID}.{X-Inkbox-Timestamp}.{raw_body}.
  3. Compute the HMAC — use HMAC-SHA256 with your signing key over the reconstructed message.
  4. Compare digests — the resulting hex digest should match the value after sha256= in the X-Inkbox-Signature header.

Python verification example

Python

Disabling webhooks

To stop receiving webhooks at a URL, delete the subscription that owns it. Delivery stops immediately.