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.

---

# Messages
description: Send and list iMessages for your agent identities, and upload media to attach

---


# Messages

Send and list iMessages. Unlike [texts](/docs/api/phone/texts), iMessages are not scoped to a phone number you own — they are scoped to the **agent identity** and keyed by `conversation_id`. The pool line carrying a conversation is managed by Inkbox and never appears in responses.

---

## Send message `POST`


Send an outbound iMessage to a connected recipient. Pass `conversation_id` to reply into an existing conversation, or `to` to address the connected recipient by number — exactly one of the two.

Outbound iMessages are **replies by design**: the recipient must have connected through the [router](/docs/api/imessage/router) and sent at least one message first. There is no cold outreach over iMessage.

Reply into a conversation:

```json
{
    "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
    "text": "On it — give me two minutes."
}
```

Or address the connected recipient directly:

```json
{
    "to": "+15555550123",
    "text": "Done!",
    "send_style": "confetti"
}
```

### Query parameters

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `agent_identity_id` | UUID | The identity to send as. Required for admin API keys when sending by `to`; ignored for [identity-scoped API keys](/docs/api-keys), which always send as their own identity |

### Request body

| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `to` | string | Conditional | E.164 number of a connected recipient. Mutually exclusive with `conversation_id` |
| `conversation_id` | UUID | Conditional | Existing conversation to reply into. Mutually exclusive with `to` |
| `text` | string \| null | No | Message body, up to 18,996 characters. Required unless `media_urls` has an item |
| `media_urls` | string[] \| null | No | Publicly fetchable media URL to attach — at most 1 entry. Use [Upload media](#upload-media) to turn raw bytes into a URL. Required unless `text` is present |
| `send_style` | string \| null | No | Expressive [send style](#send-styles) applied to the message |

### Preconditions

- **Identity opt-in.** The sending identity must have `imessage_enabled: true`; otherwise sends return `400`.
- **Recipient-first contact.** The recipient must be connected to this identity and have sent at least one message. Sending to an unconnected number returns `404`; sending into a connection where the recipient hasn't messaged yet returns `409`.
- **Connection still active.** If the recipient disconnected from the agent, sends into the old conversation return `409` until they reconnect through the router.
- **Contact rules.** Sends to a recipient blocked by the identity's [contact rules](/docs/api/imessage/contact-rules) return `403` before anything is sent.

### Rate limits

Each agent identity can send up to **100 iMessages per rolling 24-hour window**. When the cap is reached, `429` responses include:

| Header | Description |
| :--- | :--- |
| `X-RateLimit-Limit` | Maximum sends allowed per identity per 24 hours |
| `X-RateLimit-Remaining` | Remaining sends in the current window |
| `X-RateLimit-Reset` | ISO 8601 timestamp at which the oldest send in the window will fall off |
| `Retry-After` | Seconds until at least one slot frees up |

### Response (201)

The queued message, wrapped in a `message` envelope. Delivery state updates asynchronously — re-read the message, or subscribe to the [delivery-lifecycle webhooks](/docs/api/imessage/webhooks#delivery-lifecycle-events) (`imessage.sent`, `imessage.delivered`, `imessage.delivery_failed`) to track it without polling.

```json
{
    "message": {
      "id": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
      "conversation_id": "82cf24f6-78fe-48da-a673-6a75b4f4a819",
      "assignment_id": "9b2e4a68-8cb1-4f18-b97d-a2324c8b4d1f",
      "direction": "outbound",
      "remote_number": "+15555550123",
      "content": "On it — give me two minutes.",
      "message_type": "message",
      "service": "imessage",
      "send_style": null,
      "media": null,
      "was_downgraded": null,
      "status": "pending",
      "error_code": null,
      "error_message": null,
      "error_reason": null,
      "error_detail": null,
      "is_read": true,
      "is_blocked": false,
      "recipients": [
        {
          "remote_number": "+15555550123",
          "delivery_status": "pending",
          "service": "imessage",
          "error_code": null,
          "error_message": null,
          "error_reason": null,
          "error_detail": null,
          "sent_at": null,
          "delivered_at": null,
          "failed_at": null
        }
      ],
      "reactions": null,
      "created_at": "2026-06-09T14:30:00Z",
      "updated_at": "2026-06-09T14:30:00Z"
    }
}
```

### Error responses

| Status | Description |
| :--- | :--- |
| 400 | Sending identity is not enabled for iMessage, or `agent_identity_id` is missing on an admin send by `to` |
| 403 | Recipient is blocked by a contact rule on the identity |
| 404 | No connection exists for this recipient — the response tells you the connect command and router number to relay |
| 404 | `conversation_id` not found |
| 409 | The recipient hasn't messaged this agent yet, or has disconnected and must reconnect through the router |
| 422 | Invalid body: both/neither `to` and `conversation_id`, no text or media, more than one `media_urls` entry, or `text` over 18,996 characters |
| 429 | 24-hour per-identity send limit reached; respect the `Retry-After` header |

Delivery happens asynchronously after the `201`, so provider failures never surface as send errors — they arrive as `status: "error"` on the message and an [`imessage.delivery_failed`](/docs/api/imessage/webhooks#delivery-lifecycle-events) webhook.

---

## List messages `GET`


List iMessages visible to the caller, newest first, across all of the caller's conversations or narrowed to one.

### Query parameters

| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `agent_identity_id` | UUID | - | Narrow to one identity. Ignored for identity-scoped API keys, which always see their own identity |
| `conversation_id` | UUID | - | Narrow to one conversation |
| `limit` | integer | 50 | Number of results (1-200) |
| `offset` | integer | 0 | Pagination offset |
| `is_read` | boolean | - | Filter by read state. Omit for all messages |
| `is_blocked` | boolean | - | Filter by blocked state. `true` returns only blocked rows, `false` only non-blocked rows, and omitting it applies the caller's default visibility |

[Identity-scoped API keys](/docs/api-keys) never see contact-rule-blocked messages, regardless of `is_blocked`. Admin API keys and the [Inkbox Console](https://inkbox.ai/console) see blocked and non-blocked messages by default; use `is_blocked=true` for a blocked-only audit listing.

### Response (200)

Returns a `list[IMessage]`. Live tapbacks ride along on each message — see the [Message object](#message-object).

```json
[
    {
      "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,
      "is_blocked": false,
      "recipients": null,
      "reactions": [
        {
          "id": "0f6b8a5e-1ca0-4a4f-8e94-44b3e4374f9a",
          "direction": "outbound",
          "reaction": "like",
          "custom_emoji": null,
          "remote_number": "+15555550123",
          "part_index": 0,
          "created_at": "2026-06-09T14:31:00Z"
        }
      ],
      "created_at": "2026-06-09T14:30:00Z",
      "updated_at": "2026-06-09T14:31:00Z"
    }
]
```

### Error responses

| Status | Description |
| :--- | :--- |
| 400 | `agent_identity_id` names an identity that is not enabled for iMessage |
| 403 | Identity-scoped key passed an `agent_identity_id` other than its own |
| 404 | `agent_identity_id` or `conversation_id` not found, or not visible to the caller |

---

## Upload media `POST`


Upload a file and get back a URL you can pass in `media_urls` on a send. The request is `multipart/form-data` with a single `file` field.

### Request

| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `file` | file | Yes | The file to upload, up to 10 MiB. The part's content type and filename are preserved |

### Response (201)

```json
{
    "media_url": "https://media.example.com/abc123_chart.png",
    "content_type": "image/png",
    "size": 48211
}
```

### Error responses

| Status | Description |
| :--- | :--- |
| 400 | Caller's identity is not enabled for iMessage |
| 413 | File exceeds 10 MiB |
| 502 | Upstream upload failure — safe to retry |

---

## Send styles

Pass `send_style` on a send to apply one of Apple's expressive effects:

| Bubble effects | Full-screen effects |
| :--- | :--- |
| `slam` | `celebration` |
| `loud` | `shooting_star` |
| `gentle` | `fireworks` |
| `invisible` (invisible ink) | `lasers` |
| | `love` |
| | `confetti` |
| | `balloons` |
| | `spotlight` |
| | `echo` |

Send styles render on the recipient's device when the message is delivered over iMessage; they do not apply to SMS fallback.

## Message object

| Field | Type | Description |
| :--- | :--- | :--- |
| `id` | string (UUID) | Message ID |
| `conversation_id` | string (UUID) | The conversation this message belongs to |
| `assignment_id` | string (UUID) | The connection (one human ↔ one agent) carrying this conversation |
| `direction` | string | `"inbound"` or `"outbound"` |
| `remote_number` | string | The human's phone number (E.164). There is no local-number field — the pool line is managed by Inkbox |
| `content` | string \| null | Message body. Null for media-only messages |
| `message_type` | string | `"message"`, or `"carousel"` for rich multi-part messages |
| `service` | string | Transport actually used: `"imessage"`, `"sms"`, or `"rcs"` |
| `send_style` | string \| null | [Send style](#send-styles) applied to the message, if any |
| `media` | array \| null | Media attachments. Each item has `url`, `content_type`, and `size`. URLs for stored inbound media are presigned and expire after 1 hour |
| `was_downgraded` | boolean \| null | Whether delivery fell back from iMessage to SMS |
| `status` | string \| null | Delivery lifecycle: `"registered"`, `"pending"`, `"queued"`, `"accepted"`, `"sent"`, `"delivered"`, `"declined"`, `"error"`, or `"received"` for inbound |
| `error_code` | string \| null | Delivery error code, when delivery failed |
| `error_message` | string \| null | Short delivery error description |
| `error_reason` | string \| null | Delivery error reason, when reported |
| `error_detail` | string \| null | Human-readable detail accompanying the error |
| `is_read` | boolean | Read state. For inbound messages, set by [mark-read](/docs/api/imessage/conversations#mark-conversation-read) |
| `is_blocked` | boolean | Whether the message was blocked by a contact rule or by default-block in whitelist mode. Identity-scoped API keys never receive rows where this is `true` |
| `recipients` | array \| null | Per-recipient delivery state for outbound messages; null for inbound. Each entry has `remote_number`, `delivery_status`, `service`, error fields, and `sent_at` / `delivered_at` / `failed_at` timestamps |
| `reactions` | array \| null | Live [tapbacks](/docs/api/imessage/reactions) targeting this message, oldest first. Each entry has `id`, `direction`, `reaction`, `custom_emoji`, `remote_number`, `part_index`, and `created_at` |
| `created_at` | string (ISO 8601) | Creation timestamp |
| `updated_at` | string (ISO 8601) | Last update timestamp |
