> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.agentphone.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.agentphone.ai/_mcp/server.

# Messages

> Send and receive SMS, MMS, iMessage, and WhatsApp — text, media, threaded replies, effects, reactions, and group chats

Use `POST /v1/messages` to send through your number's messaging service: SMS/MMS, iMessage, WhatsApp, or an enabled RCS Business Messaging sender. The sending number determines which services are available. A standard SMS number does not gain iMessage by choosing a different recipient.

Messages are stored in conversations. The send response confirms acceptance; inspect message history for delivery status and `deliveredChannel` when the network reports a fallback. The response's `channel` alone does not prove the recipient's final transport.

## In this guide

* **Channels**: what SMS/MMS vs iMessage support
* **Sending messages**: send text and media, and choose which number to send from
* **SMS & MMS**: cellular texting, images, and 10DLC for outbound
* **iMessage**: threaded replies, send effects, reactions, group chats, typing indicators, backgrounds, and [limits](#imessage-limits)
* **WhatsApp**: templates, buttons, list menus, and the 24-hour window — see [WhatsApp](/documentation/guides/whatsapp)
* **Conversations & history**: list/get/update conversations and read message history
* **Webhooks**: inbound messages arrive via webhook — see [Webhooks](/documentation/guides/webhooks)

## Channels

Message history can report `sms`, `mms`, `imessage`, `whatsapp`, or `rcs`. Omit `channel` to use the selected number's default transport. Use `channel: "whatsapp"` for its WhatsApp connection, or `channel: "rcs"` for an onboarded RCS sender. Do not pass `channel: "sms"` or `channel: "imessage"`; those are not supported request overrides. RCS is concierge-only and intentionally absent from the public schema/generated SDK types; use the HTTP examples in the [RCS guide](/documentation/guides/rcs) after onboarding.

### SMS & MMS

Text messaging over the cellular network. Recipient reachability, country restrictions, and MMS support still apply.

* Outbound requires [SMS registration](/sms-registration) (carrier requirement for US numbers)
* Inbound works immediately — no registration needed
* Media sends and arrives as MMS (`channel: "mms"`) — pass `media_url` or `media_urls` when sending

### iMessage

Apple's messaging protocol. Delivered over the internet to Apple devices, with automatic fallback to RCS or SMS for non-Apple recipients.

* No 10DLC registration required
* Supports reactions (tapbacks), threaded replies, send effects, rich media, image carousels, and group chats
* `channel` is `"imessage"` for messages delivered via iMessage
* The standard outbound-first ceiling is 50 new contacts/day/line. Replies have separate conversation and safety restrictions. See [iMessage limits](#imessage-limits)

### WhatsApp

Your own WhatsApp Business Account, connected once through the dashboard. A separate network from the cellular and Apple channels, with its own rules.

* Requires `channel: "whatsapp"` on the send. There is no automatic fallback to or from WhatsApp
* Free-form messages only reach someone who messaged you in the last 24 hours. Outside that window you need an approved template
* Supports quote replies, up to 3 reply buttons, list menus, link buttons, media, and reactions
* Applicable WhatsApp message charges are billed by Meta to your WABA. See [WhatsApp billing](/documentation/guides/whatsapp#billing)
* Full detail in the [WhatsApp guide](/documentation/guides/whatsapp)

> **Note**
>
> An iMessage line can use its fallback transport for supported non-Apple recipients. This is different from [RCS Business Messaging](/documentation/guides/rcs), which uses a separately onboarded branded sender. Do not assume every iMessage feature works after fallback.

### Capabilities

Check `capabilities` on `GET /v1/conversations/{id}` before enabling sending, attachments, or reactions in your UI. The API returns an error for unsupported feature/number combinations; do not rely on features being silently discarded.

| Feature                | SMS/MMS line                             | Eligible iMessage line                            | WhatsApp                                 |
| ---------------------- | ---------------------------------------- | ------------------------------------------------- | ---------------------------------------- |
| Text and media         | Yes, within carrier limits               | Yes                                               | Yes, within Meta's window/template rules |
| Reactions              | No                                       | Classic tapbacks; custom emoji on supported lines | Yes                                      |
| Group threads          | Available on supported carrier MMS lines | Yes                                               | No                                       |
| Quote/threaded replies | No                                       | On supported lines                                | Yes                                      |
| Send effects           | No                                       | Yes                                               | No                                       |
| Chat backgrounds       | No                                       | Supported one-to-one conversations                | No                                       |
| Buttons and list menus | No                                       | No                                                | Yes                                      |

See [RCS Business Messaging](/documentation/guides/rcs) for branded cards, carousels, suggested replies, and SMS fallback setup.

## Sending messages

Select an eligible sending number and use its default transport, or specify a supported channel override. Use explicit `number_id` selection when the sender matters.

```
POST /v1/messages
```

### Request body

| Field                 | Type   | Required                  | Description                                                                                                                                                       |
| --------------------- | ------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `to_number`           | string | Unless using `recipients` | Recipient in E.164, or the returned group identifier for an existing [group chat](#group-chats).                                                                  |
| `recipients`          | array  | No                        | Start a new supported group; mutually exclusive with `to_number`. See [Group chats](#group-chats).                                                                |
| `channel`             | string | No                        | `whatsapp` or `rcs`; omit for the number's default transport.                                                                                                     |
| `body`                | string | Yes                       | Message text. May be an empty string when sending media only.                                                                                                     |
| `agent_id`            | string | No                        | Agent to send as. Used to pick the sending number when `from_number`/`number_id` are omitted.                                                                     |
| `from_number`         | string | No                        | Exact number to send from (E.164). Must be one of your numbers.                                                                                                   |
| `number_id`           | string | No                        | ID of the number to send from. Alternative to `from_number`.                                                                                                      |
| `media_url`           | string | No                        | A single image, video, or file URL to attach.                                                                                                                     |
| `media_urls`          | array  | No                        | Multiple media URLs (delivered as an image carousel on iMessage).                                                                                                 |
| `reply_to_message_id` | string | No                        | Quote an earlier message on supported iMessage lines or WhatsApp. The parent must belong to this same number and chat. See [Threaded replies](#threaded-replies). |
| `send_style`          | string | No                        | iMessage only. Apply an expressive screen or bubble effect. See [Send effects](#send-effects).                                                                    |

### Choosing the sending number

Specify the sender one of three ways, in order of precedence:

1. **`from_number`** — the exact E.164 number you want to send from.
2. **`number_id`** — the ID of one of your numbers.
3. **`agent_id`** — automatically selects an eligible attached number. Selection can prefer a number with richer messaging capabilities; do not rely on attachment order.

Pass at least one. Explicit sender selection takes priority over automatic selection, but supplied selectors must agree: a conflicting number or agent returns an error. A `number_id` used for sending must identify an attached number.

### Example

```bash
curl -X POST "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_abc123",
    "to_number": "+15559876543",
    "body": "Your order #4521 shipped today and arrives Thursday."
  }'
```

```json
{
  "id": "msg_abc123",
  "status": "sent",
  "channel": "imessage",
  "from_number": "+15551234567",
  "to_number": "+15559876543",
  "media_urls": [],
  "reply_to_message_id": null,
  "reply_parent_unresolved": null
}
```

**`Python (agentphone SDK)`**

```python title="Python (agentphone SDK)"
from agentphone import AgentPhone

client = AgentPhone(api_key="YOUR_API_KEY")

client.messages.send(
    agent_id="agt_abc123",
    to_number="+15559876543",
    body="Your order #4521 shipped today and arrives Thursday.",
)
```

**`TypeScript (agentphone SDK)`**

```typescript title="TypeScript (agentphone SDK)"
import { AgentPhoneClient } from "agentphone";

const client = new AgentPhoneClient({ token: "YOUR_API_KEY", maxRetries: 0 });

await client.messages.sendMessage({
  agent_id: "agt_abc123",
  to_number: "+15559876543",
  body: "Your order #4521 shipped today and arrives Thursday.",
});
```

### Sending media

Use `media_urls` for attachments (do not also supply `media_url`). Use an empty `body` for media-only messages. WhatsApp and group sends accept one attachment; other supported sends accept up to 20. Some iMessage lines reject text alongside a multi-image carousel, so the example sends only images. Send accompanying text separately. Native iMessage voice memos also require one audio file and an empty body.

```bash
curl -X POST "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_abc123",
    "to_number": "+15559876543",
    "body": "",
    "media_urls": [
      "https://example.com/room-1.jpg",
      "https://example.com/room-2.jpg"
    ]
  }'
```

On iMessage, multiple URLs arrive as a single image carousel. Over SMS the message is delivered as MMS. See [Media](#media) for how inbound attachments are represented.

Media URLs must be publicly reachable over HTTPS. Over carrier MMS, attachments must be an image (JPEG, PNG, GIF), audio (MP3, M4A, AMR, WAV), video (MP4, 3GP), vCard, or PDF, and within your sending account’s attachment limit (1 MB, 3.5 MB, or 5 MB depending on its MMS route). Keep files at or below 1 MB for the broadest compatibility — other types are rejected with a `400` because carriers won't deliver them. iMessage accepts most common file types. Delivery of audio and video over MMS depends on the recipient's carrier and handset; for large or unusual files, send a link instead.

## SMS & MMS

SMS and MMS use the cellular network. Standard SMS lines send through their carrier route; iMessage lines can also use fallback transport when available.

* **Text** from a standard SMS line is recorded as `channel: "sms"`. Destination and account restrictions still apply.
* **Media** (images, audio, video, vCards, PDFs) is sent as MMS: pass `media_url` or `media_urls`; both you and the recipient see it as `channel: "mms"`.
* **Outbound to US numbers requires [SMS registration](/sms-registration)** (a carrier requirement); inbound needs no registration. See [Messaging Rate Limits](/documentation/reference/messaging-rate-limits) for throughput.
* `send_style` and `reply_to_message_id` are rejected with `400` on unsupported standard SMS lines. Remove those fields for an ordinary SMS send.

## iMessage

Use an eligible iMessage line for the rich features below. Some older lines do not support threaded replies, custom emoji, or backgrounds; unsupported requests return a clear error. Recipient/device capabilities also affect what is displayed.

### iMessage limits

Outbound iMessage lines have a standard ceiling of **50 new contacts per day per line**. Daily caps, unanswered-message limits, inactivity rules, and account safety pauses are separate controls. A retry does not clear a daily or reply-required limit.

See [Messaging rate limits](/documentation/reference/messaging-rate-limits#imessage) for the full rules and [iMessage setup](/imessage) for purchasing and activation.

### Threaded replies

On iMessage you can send a message as an **inline reply** to an earlier message, so it renders threaded under the original in the recipient's chat. Pass the AgentPhone message ID of the parent as `reply_to_message_id`.

```bash
curl -X POST "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_abc123",
    "to_number": "+15559876543",
    "body": "Yes, Thursday works.",
    "reply_to_message_id": "msg_001"
  }'
```

The parent ID is any `Message.id` from the conversation, such as `data.messageId` on an inbound [webhook](/documentation/guides/webhooks) message or a message from [Get conversation messages](#get-conversation-messages). The response echoes back `reply_to_message_id`, plus a `reply_parent_unresolved` flag:

| Field                     | Meaning                                                                                                                                                      |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `reply_to_message_id`     | The parent you threaded under, echoed back.                                                                                                                  |
| `reply_parent_unresolved` | `true` if the parent couldn't be matched to a real iMessage (e.g. it was sent over SMS, or is too old). The message still sends, just not visually threaded. |

> **Note**
>
> Supported iMessage lines can report `reply_parent_unresolved: true` when the provider cannot resolve the parent. Standard SMS lines reject the field. WhatsApp quote replies use the same request field; see the WhatsApp guide.

### Send effects

iMessage supports expressive **send effects** — the screen and bubble animations you get from the iMessage compose screen (Slam, Confetti, Fireworks, etc.). Set `send_style` to apply one.

```bash
curl -X POST "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_abc123",
    "to_number": "+15559876543",
    "body": "You are all set — see you Thursday!",
    "send_style": "confetti"
  }'
```

| `send_style`  | Effect                                     |
| ------------- | ------------------------------------------ |
| `slam`        | Bubble slams down onto the screen          |
| `loud`        | Bubble grows large and shakes              |
| `gentle`      | Bubble arrives small and soft              |
| `invisible`   | Invisible ink — recipient swipes to reveal |
| `confetti`    | Full-screen confetti                       |
| `balloons`    | Full-screen balloons                       |
| `fireworks`   | Full-screen fireworks                      |
| `celebration` | Full-screen celebration burst              |
| `lasers`      | Full-screen laser light show               |
| `spotlight`   | Spotlight focuses on the message           |
| `echo`        | The message multiplies across the screen   |
| `love`        | A heart inflates from the bubble           |

> **Note**
>
> Send effects require an iMessage-capable sending number and a recipient that can display the effect. Standard SMS numbers reject `send_style` with `400`. A fallback transport may not display the effect.

### Reactions

iMessage supports tapback reactions such as hearts and thumbs up. The same endpoint also supports [WhatsApp reactions](/documentation/guides/whatsapp). Standard SMS/MMS numbers do not support this endpoint.

#### Send a reaction

```
POST /v1/messages/{message_id}/reactions
```

| Field      | Type   | Required | Description                                                                                                                                                    |
| ---------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reaction` | string | Yes      | Classic tapback: `love`, `like`, `dislike`, `laugh`, `emphasize`, `question`. A single custom emoji is also supported on eligible iMessage lines and WhatsApp. |

```bash
curl -X POST "https://api.agentphone.ai/v1/messages/msg_001/reactions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reaction": "love"}'
```

```json
{
  "id": "rxn_abc123",
  "reaction_type": "love",
  "message_id": "msg_001",
  "channel": "imessage"
}
```

Reacting to an image in a carousel works the same way — use the message ID of the specific image.

> **Warning**
>
> Reactions require an eligible iMessage line or a WhatsApp connection. Standard SMS/MMS numbers reject reaction requests with `400`.

#### Receive reactions

When someone reacts to a message your agent sent, an `agent.reaction` webhook event is fired. See [Webhooks](/documentation/guides/webhooks#events) for the payload format.

### Group chats

Group threads use a conversation marked `isGroup: true`, keyed by a group identifier instead of one participant. They are supported on eligible iMessage lines and some carrier MMS lines. WhatsApp and branded RCS do not support group sending through this API. Group messages accept at most one attachment.

Group conversations and messages carry extra fields:

| Field              | Where        | Description                                                                                                                         |
| ------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `isGroup`          | conversation | `true` for group threads. Stable for the life of the thread.                                                                        |
| `groupId`          | conversation | Group identifier. Preserve it exactly and use it when replying; do not construct it or assume every transport uses a `grp_` prefix. |
| `groupName`        | conversation | Group display name when known.                                                                                                      |
| `groupIconUrl`     | conversation | Group icon URL when known.                                                                                                          |
| `participants`     | conversation | Roster as last seen: an array of `{ identifier, name }`. Refreshed as members join or leave.                                        |
| `senderIdentifier` | message      | The member who sent an inbound message. `null` on one-to-one messages.                                                              |

#### Start a group

On a number that supports group creation, pass `recipients` instead of `to_number`. Eligible iMessage lines accept 2–32 distinct recipients; carrier MMS groups accept 2–10 phone numbers. Some older iMessage lines can reply to existing groups but cannot create them.

```bash
curl --fail-with-body "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer $AGENTPHONE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"number_id":"YOUR_NUMBER_ID","recipients":["+14155550123","+12125550199"],"body":"Welcome to the group."}'
```

Save the response's `to_number` as the group address for subsequent sends. An unsupported line returns `422`; do not retry unchanged requests.

#### Replying to a group

To post into the group thread, call [`POST /v1/messages`](#sending-messages) with `to_number` set to the exact **`groupId`**, not an individual member's number. Sending to a member's number would start a separate one-to-one conversation with that person.

```bash
curl -X POST "https://api.agentphone.ai/v1/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_abc123",
    "to_number": "grp_abc123",
    "body": "Friday at 7 works for me!"
  }'
```

On supported iMessage lines, group replies can use [threaded replies](#threaded-replies) and [send effects](#send-effects). To reply directly to a specific member's message in the thread, pass that message's ID as `reply_to_message_id`.

Inbound group messages are delivered to your webhook with the full group roster and the sending member inline, so you can attribute and route without a follow-up fetch. See [Webhooks](/documentation/guides/webhooks#group-chats) for the event payload and a routing example.

### Typing indicator

Show a typing bubble in an iMessage conversation. This signals to the recipient that your agent is composing a response.

```
POST /v1/conversations/{conversation_id}/typing
```

The request body is empty. The indicator auto-expires after a few seconds, so there's no "stop typing" call needed. Send it right before your agent starts generating a response.

```bash
curl -X POST "https://api.agentphone.ai/v1/conversations/conv_def456/typing" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "conversationId": "conv_def456",
  "channel": "imessage",
  "status": "sent"
}
```

> **Note**
>
> Typing indicators require an eligible iMessage number; standard SMS numbers return `400`. A successful response confirms the request was sent, not that the recipient displayed a typing bubble.

### Chat backgrounds

Set a custom background image for an iMessage conversation. The background is visible to the recipient inside the chat, and is a nice touch for branded or themed experiences.

#### Set a background

```
POST /v1/conversations/{conversation_id}/background
```

| Field       | Type   | Required | Description                                      |
| ----------- | ------ | -------- | ------------------------------------------------ |
| `image_url` | string | Yes      | Publicly accessible URL of the background image. |

```bash
curl -X POST "https://api.agentphone.ai/v1/conversations/conv_def456/background" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"image_url": "https://example.com/brand-background.jpg"}'
```

```json
{
  "conversationId": "conv_def456",
  "channel": "imessage",
  "hasBackground": true,
  "backgroundId": "bg_abc123",
  "backgroundVersion": 1,
  "changed": true
}
```

The `changed` flag is `false` when the requested image was already set, so re-sending the same background is a safe no-op. `backgroundVersion` increments each time the image actually changes.

#### Remove a background

```
DELETE /v1/conversations/{conversation_id}/background
```

```bash
curl -X DELETE "https://api.agentphone.ai/v1/conversations/conv_def456/background" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

> **Note**
>
> Chat backgrounds require a supported one-to-one iMessage conversation. Standard SMS lines, unsupported iMessage lines, and group conversations return `400`. Removing an eligible background returns `204` with no body.

## WhatsApp

WhatsApp has enough of its own rules to get a page of its own. The short version:

* Send with `channel: "whatsapp"`. Everything goes through `POST /v1/messages` as usual
* Free-form sends need an open 24-hour window, meaning the customer messaged you recently. `GET /v1/conversations/{id}` reports `capabilities.whatsappWindowExpiresAt` so you can check before sending
* Outside the window, use an approved template
* Interactive types: `buttons` (up to 3, with an optional media header), `list` (up to 10 rows), and `cta` for a single link button
* Button and list taps arrive as normal inbound messages carrying `replyTo`, which points at the message that offered the choice

See the [WhatsApp guide](/documentation/guides/whatsapp) for templates, connecting an account, billing, and the full limits.

## Conversations & history

A conversation is the thread between one of your numbers and an external participant (a single contact, or a group on iMessage). Each tracks full message history and supports custom metadata for storing session state. These endpoints are channel-agnostic.

## List conversations

List all conversations for this project, sorted by most recent activity.

```
GET /v1/conversations
```

### Query parameters

| Parameter | Type    | Required | Default | Description                           |
| --------- | ------- | -------- | ------- | ------------------------------------- |
| `limit`   | integer | No       | 20      | Number of results to return (max 100) |
| `offset`  | integer | No       | 0       | Number of results to skip (min 0)     |

### Example

```bash
curl -X GET "https://api.agentphone.ai/v1/conversations?limit=10&offset=0" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": [
    {
      "id": "conv_def456",
      "agentId": "agt_abc123",
      "phoneNumberId": "num_xyz789",
      "phoneNumber": "+15551234567",
      "participant": "+15559876543",
      "lastMessageAt": "2025-01-15T12:00:00Z",
      "lastMessagePreview": "Thanks for your help!",
      "messageCount": 8,
      "metadata": {
        "customerName": "Jane Doe",
        "orderId": "ORD-12345"
      },
      "createdAt": "2025-01-15T11:30:00Z"
    }
  ],
  "hasMore": false,
  "total": 1
}
```

## Get conversation

Get a conversation with its recent messages.

```
GET /v1/conversations/{conversation_id}
```

### Query parameters

| Parameter       | Type    | Required | Default | Description                                    |
| --------------- | ------- | -------- | ------- | ---------------------------------------------- |
| `message_limit` | integer | No       | 50      | Number of recent messages to include (max 100) |

### Example

```bash
curl -X GET "https://api.agentphone.ai/v1/conversations/conv_def456?message_limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Update conversation

Update conversation metadata (state). Use this to store custom context for AI agents, such as customer information, business context, or session state.

The metadata is included in webhook payloads as `conversationState`, enabling your AI backend to maintain context across messages.

```
PATCH /v1/conversations/{conversation_id}
```

### Request body

| Field      | Type           | Required | Description                                                                       |
| ---------- | -------------- | -------- | --------------------------------------------------------------------------------- |
| `metadata` | object or null | No       | Custom key-value metadata to store with the conversation. Set to `null` to clear. |

### Example

```bash
curl -X PATCH "https://api.agentphone.ai/v1/conversations/conv_def456" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata": {
      "customerName": "Jane Doe",
      "orderId": "ORD-12345",
      "topic": "order_tracking",
      "resolved": false
    }
  }'
```

## Get conversation messages

Get paginated messages for a conversation. Use `before`/`after` for cursor-based pagination through large message histories.

```
GET /v1/conversations/{conversation_id}/messages
```

### Query parameters

| Parameter | Type                      | Required | Default | Description                                      |
| --------- | ------------------------- | -------- | ------- | ------------------------------------------------ |
| `limit`   | integer                   | No       | 50      | Number of messages to return (max 200)           |
| `before`  | string (datetime) or null | No       | null    | Return messages before this timestamp (ISO 8601) |
| `after`   | string (datetime) or null | No       | null    | Return messages after this timestamp (ISO 8601)  |

### Example

```bash
curl -X GET "https://api.agentphone.ai/v1/conversations/conv_def456/messages?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": [
    {
      "id": "msg_001",
      "body": "Hi, I need help with my order",
      "fromNumber": "+15559876543",
      "toNumber": "+15551234567",
      "direction": "inbound",
      "channel": "sms",
      "mediaUrl": null,
      "receivedAt": "2025-01-15T11:30:00Z"
    }
  ],
  "hasMore": true
}
```

### Message fields

| Field                         | Type           | Description                                                                               |
| ----------------------------- | -------------- | ----------------------------------------------------------------------------------------- |
| `id`                          | string         | Unique message ID                                                                         |
| `body`                        | string         | Message text (empty string for image-only messages)                                       |
| `fromNumber`                  | string         | Sender's phone number (E.164)                                                             |
| `toNumber`                    | string         | Recipient's phone number (E.164)                                                          |
| `direction`                   | string         | `"inbound"` or `"outbound"`                                                               |
| `channel`                     | string         | `"sms"`, `"mms"`, `"imessage"`, `"whatsapp"`, or `"rcs"`                                  |
| `status`                      | string or null | Delivery state, such as `sent`, `delivered`, or `failed`; inbound messages use `received` |
| `failureReason` / `errorCode` | string or null | Failure explanation and machine code when available                                       |
| `deliveredChannel`            | string or null | Actual transport reported by a delivery receipt when different from `channel`             |
| `mediaUrls`                   | array          | All available attachment URLs; `mediaUrl` is the first                                    |
| `mediaUrl`                    | string or null | CDN URL of an attached image, video, or file                                              |
| `receivedAt`                  | string         | ISO 8601 timestamp                                                                        |

## Media

When someone sends an image, video, or file, the message includes a `mediaUrl` pointing to the hosted media. The `body` may be empty if only media was sent.

```json
{
  "id": "msg_002",
  "body": "",
  "fromNumber": "+15559876543",
  "toNumber": "+15551234567",
  "direction": "inbound",
  "channel": "imessage",
  "mediaUrl": "https://storage.googleapis.com/inbound-file-store/abc123_IMG_5414.png",
  "receivedAt": "2025-01-15T12:01:00Z"
}
```

Read `mediaUrls` to handle multiple attachments, with `mediaUrl` as the single-attachment compatibility field. Depending on transport, multiple images can also arrive as separate message records.

Media works on both iMessage and MMS (SMS with images). When an image arrives via SMS, the `channel` is `"mms"`.