Inkbox

> # Documentation index
> Fetch the complete documentation index at: https://inkbox.ai/sitemap.xml
> Use this file to discover all available pages before exploring further.

---

# Webhooks
description: Receive real-time HTTP POST callbacks for inbound iMessages, tapbacks, and outbound delivery status

---


# Webhooks

Webhooks let you receive HTTP POST callbacks the moment a connected human messages or tapbacks your agent, and as your agent's outbound messages move through their delivery lifecycle. iMessage events are delivered via the [Webhook Subscriptions API](/docs/api/webhooks/subscriptions) — attach a subscription to the **agent identity** with the subset of `imessage.*` events you want.

The agent identity is the subscription owner (rather than a mailbox or phone number) because iMessage conversations ride a shared pool of lines that aren't org resources — the identity is the stable thing the events belong to.

## Event types

| Event | Description |
| :--- | :--- |
| `imessage.received` | A connected human sent your agent a message |
| `imessage.reaction_received` | A connected human tapbacked one of the thread's messages |
| `imessage.sent` | An outbound message from your agent was accepted for delivery |
| `imessage.delivered` | An outbound message reached the recipient's device |
| `imessage.delivery_failed` | An outbound message failed to deliver — the payload carries the error fields |

Inbound traffic rejected by a [contact-rule](/docs/api/imessage/contact-rules) block or by default-block in whitelist mode emits **no** webhooks. Blocked messages are stored, and [admin API keys](/docs/api-keys) or the [Inkbox Console](https://inkbox.ai/console) can audit them with [`is_blocked=true`](/docs/api/imessage/messages#list-messages). Fan-out also pauses while the identity is paused or has `imessage_enabled: false`. Tapback removals do not fire an event; the next message read simply no longer includes the removed tapback.

## Subscribing

**cURL**

```bash
curl -X POST "https://inkbox.ai/api/v1/webhooks/subscriptions" \\
    -H "X-API-Key: YOUR_API_KEY" \\
    -H "Content-Type: application/json" \\
    -d '{
      "agent_identity_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "url": "https://yourapp.example.com/webhooks/inkbox",
      "event_types": ["imessage.received", "imessage.reaction_received"]
    }'
```

**Python**

```python
inkbox.webhooks.subscriptions.create(
    agent_identity_id=identity.id,
    url="https://yourapp.example.com/webhooks/inkbox",
    event_types=["imessage.received", "imessage.reaction_received"],
)
```

**TypeScript**

```typescript
await inkbox.webhooks.subscriptions.create({
    agentIdentityId: identity.id,
    url: "https://yourapp.example.com/webhooks/inkbox",
    eventTypes: ["imessage.received", "imessage.reaction_received"],
});
```

Subscribe to any subset — add `imessage.sent`, `imessage.delivered`, and `imessage.delivery_failed` to the same subscription to track outbound delivery. See [Webhook Subscriptions](/docs/api/webhooks/subscriptions) for listing, updating, and deleting subscriptions, and [Signing keys](/docs/signing-keys) for verifying that deliveries came from Inkbox.

## Payload envelope

Every delivery POSTs a JSON envelope:

| Field | Type | Description |
| :--- | :--- | :--- |
| `event_type` | string | One of the five `imessage.*` event types |
| `timestamp` | string (ISO 8601) | When the event occurred |
| `data` | object | The event payload — see below |

`data` carries exactly one of `message` (for `imessage.received` and the delivery-lifecycle events) or `reaction` (for `imessage.reaction_received`), plus peer-resolution lists:

| Field | Type | Description |
| :--- | :--- | :--- |
| `data.message` | object \| null | The stored [message](/docs/api/imessage/messages#message-object) on `imessage.received`, `imessage.sent`, `imessage.delivered`, and `imessage.delivery_failed`; null on reaction events |
| `data.reaction` | object \| null | The stored [reaction](/docs/api/imessage/reactions#reaction-object) on `imessage.reaction_received`; null on message events |
| `data.contacts` | array | [Contact](/docs/api/contacts) matches for the human's number, visible to the receiving identity. Always present, possibly empty — each entry has `id` and `name` |
| `data.agent_identities` | array | Identity matches when the human's number belongs to another active agent identity in your org. Always present, possibly empty — each entry has `id`, `agent_handle`, and `display_name` |

## imessage.received

```json
{
    "event_type": "imessage.received",
    "timestamp": "2026-06-09T14:30:00Z",
    "data": {
      "message": {
        "id": "1a90e8b0-0e1e-485f-b316-28f7dfa96afd",
        "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
        "assignment_id": "9b2e4a68-8cb1-4f18-b97d-a2324c8b4d1f",
        "direction": "inbound",
        "remote_number": "+15555550123",
        "content": "Can you move my 3pm?",
        "message_type": "message",
        "service": "imessage",
        "send_style": null,
        "media": null,
        "was_downgraded": null,
        "status": "received",
        "error_code": null,
        "error_message": null,
        "error_reason": null,
        "error_detail": null,
        "is_read": false,
        "recipients": null,
        "reactions": null,
        "created_at": "2026-06-09T14:30:00Z",
        "updated_at": "2026-06-09T14:30:00Z"
      },
      "reaction": null,
      "contacts": [
        { "id": "c1d2e3f4-a5b6-7890-abcd-ef1234567890", "name": "Jordan Smith" }
      ],
      "agent_identities": []
    }
}
```

The message shape matches the [Message object](/docs/api/imessage/messages#message-object), with one difference: webhook payloads carry no `is_blocked` field, because blocked messages never reach the webhook at all.

## imessage.reaction_received

```json
{
    "event_type": "imessage.reaction_received",
    "timestamp": "2026-06-09T14:32:00Z",
    "data": {
      "message": null,
      "reaction": {
        "id": "5d2c9f4a-3b21-47e0-9c8d-1f6a2b3c4d5e",
        "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
        "assignment_id": "9b2e4a68-8cb1-4f18-b97d-a2324c8b4d1f",
        "target_message_id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
        "direction": "inbound",
        "reaction": "custom",
        "custom_emoji": "🌴",
        "remote_number": "+15555550123",
        "part_index": 0,
        "created_at": "2026-06-09T14:32:00Z",
        "updated_at": "2026-06-09T14:32:00Z"
      },
      "contacts": [],
      "agent_identities": []
    }
}
```

`reaction` can be any of the classic six or `"custom"` with the literal emoji in `custom_emoji`. Remember the replacement rule: a new tapback from the same human on the same message replaces their previous one, so treat the latest event as the current state for that sender.

## Delivery lifecycle events

Outbound messages fire an event on each message-level status transition: `imessage.sent` when the message is accepted for delivery, then `imessage.delivered` when it reaches the recipient's device, or `imessage.delivery_failed` if delivery fails. Only outbound messages produce these events, and only when the message as a whole changes status — per-recipient bookkeeping that doesn't move the message's own `status` does not fire.

The three payloads share one shape and differ only in `event_type`, the message's `status`, and the error fields:

```json
{
    "event_type": "imessage.delivery_failed",
    "timestamp": "2026-06-09T14:35:02Z",
    "data": {
      "message": {
        "id": "5b1d8f7c-3f44-4af0-9a07-3a4f0d8f6a31",
        "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
        "assignment_id": "9b2e4a68-8cb1-4f18-b97d-a2324c8b4d1f",
        "direction": "outbound",
        "remote_number": "+15555550123",
        "content": "On it — sending the report now.",
        "message_type": "message",
        "service": "imessage",
        "send_style": null,
        "media": null,
        "was_downgraded": false,
        "status": "error",
        "error_code": "22",
        "error_message": "Message failed to send",
        "error_reason": "recipient_unavailable",
        "error_detail": "The recipient could not be reached over iMessage.",
        "is_read": true,
        "recipients": [
          {
            "remote_number": "+15555550123",
            "delivery_status": "error",
            "service": "imessage",
            "error_code": null,
            "error_message": null,
            "error_reason": null,
            "error_detail": null,
            "sent_at": "2026-06-09T14:35:01Z",
            "delivered_at": null,
            "failed_at": "2026-06-09T14:35:02Z"
          }
        ],
        "reactions": null,
        "created_at": "2026-06-09T14:35:00Z",
        "updated_at": "2026-06-09T14:35:02Z"
      },
      "reaction": null,
      "contacts": [],
      "agent_identities": []
    }
}
```

On `imessage.sent` the message arrives with `status: "sent"`, on `imessage.delivered` with `status: "delivered"`, both with the error fields null. On `imessage.delivery_failed` the message's `status` is `"declined"` or `"error"` and the error fields describe what went wrong.

## Delivery semantics

- Deliveries are fire-and-forget HTTP POSTs; your response status is logged but does not affect message processing. Respond `2xx` quickly and process asynchronously.
- Each subscription on the identity receives its own POST per event, independently.
- Verify deliveries with your [signing key](/docs/signing-keys) before trusting the payload.

## Additional resources

- [iMessage guide](/docs/capabilities/imessage)
- [Webhook Subscriptions API](/docs/api/webhooks/subscriptions)
- [Webhooks overview](/docs/webhooks)
