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

# Messages

> Send WhatsApp messages and quoted replies with explicit delivery state

Paths on this page are relative to `https://inkbox.ai/api/v1/whatsapp`. Supply the selected `agent_identity_id` as a query parameter when needed by your credential.

## Send a message

```text theme={null}
POST /messages
```

Select either one E.164 string in `to` for a direct message or an existing direct-message or group `conversation_id`, never both. The channel must have approved access and be ready. A new request returns `201` with a `message` object after acceptance; delivery happens asynchronously. A ready channel can still reject a quoted reply, reaction, group send, or `gif_playback` send with `409 whatsapp_capability_not_ready` when that operation is unavailable. That rejection admits nothing; ordinary direct messages can remain available. Do not replace a rejected quote with an unquoted send.

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `to` | string | Conditional | Exactly one E.164 number; exclusive with `conversation_id` |
| `conversation_id` | UUID | Conditional | Reply in an existing direct-message or group conversation; exclusive with `to` |
| `text` | string | Conditional | Within current [`whatsapp.max_text_length`](https://inkbox.ai/docs/api/whatsapp/limits), at most 4,000 Unicode code points; required without media |
| `media_ids` | UUID\[] | Conditional | At most one owned [uploaded media ID](https://inkbox.ai/docs/api/whatsapp/media.md); required without nonblank text |
| `gif_playback` | boolean | No | Defaults to `false`; play an uploaded `video/mp4` attachment as an animation in direct or group conversations; other content types return `422` |
| `reply_to_message_id` | UUID | No | Quote an authorized message in the selected `conversation_id`; not accepted with `to` |
| `plain_reply_fallback` | boolean | No | Defaults to `true`; `false` requires a native quoted reply. Ignored without `reply_to_message_id`. |

All arrays, including singleton arrays, and comma-separated lists are rejected for `to`. There is no fanout. A group `conversation_id` must identify an authorized existing group where the agent has received a message from another participant. No group creation or sending by group name is supported. Unknown request fields are rejected. WhatsApp has no send-style or partial-message reaction fields.

Group sends without the required incoming message return `409 whatsapp_group_reply_required`. Group sends can also return `409 whatsapp_capability_not_ready` when unavailable. Neither rejection admits a message; keep the same request and idempotency key when retrying after the prerequisite is met.

Use an authorized [`reply_to_message_id`](https://inkbox.ai/docs/api/whatsapp/threads) for a native quoted reply. If the quote is unavailable, the API rejects the request instead of silently sending an unquoted message.

In a direct conversation, before that recipient's first WhatsApp reply, the initial policy permits up to three opening messages of at most 159 Unicode code points each. Links, phone numbers, media, and email addresses are not allowed by default. The server checks the current [opening-message policy](https://inkbox.ai/docs/api/whatsapp/limits.md) on every send and returns `422` when it rejects the draft.

```bash cURL theme={null}
curl -X POST \
  "https://inkbox.ai/api/v1/whatsapp/messages?agent_identity_id=11111111-1111-4111-8111-111111111111" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: project-brief-acknowledgment" \
  -H "Prefer: idempotency-replay" \
  -d '{"to":"+15555550123","text":"I received your project brief."}'
```

```python Python theme={null}
message = identity.send_whatsapp(
    to="+15555550123",
    text="I received your project brief.",
    idempotency_key="project-brief-acknowledgment",
)
current = identity.get_whatsapp_message(message.id)
```

```typescript TypeScript theme={null}
const message = await identity.sendWhatsApp({
  to: "+15555550123",
  text: "I received your project brief.",
  idempotencyKey: "project-brief-acknowledgment",
});
const current = await identity.getWhatsAppMessage(message.id);
```

```bash CLI theme={null}
inkbox whatsapp send --identity my-agent --to +15555550123 \
  --text "I received your project brief." \
  --idempotency-key project-brief-acknowledgment
```

To reply, replace `to` with `conversation_id` in raw requests, `conversation_id` in Python, or `conversationId` in TypeScript. The CLI accepts `--conversation-id` instead of `--to`.

### Retry without duplicating

Use one `Idempotency-Key` for each intended message and keep the same input on retry. Also send `Prefer: idempotency-replay` in raw requests; SDKs do this automatically. A replay returns the original accepted response, not the newest delivery state.

If you lose the response, use [message-send lookup](https://inkbox.ai/docs/api/message-sends.md#recover-the-original-message-id) with `sender_kind=whatsapp_identity`, the agent's UUID as `sender_id`, and `operation=whatsapp.send`. Lookup is read-only. A missing result is not permission to create a new key. Retain any media ID instead of uploading the file again.

## Get a message

```text theme={null}
GET /messages/{message_id}
```

Returns a flat message object. Re-read the original message to check progress after sending. Status freshness is reported by `status_observed_at` and `status_refresh_pending`; a pending refresh does not mean the latest remote state is already known.

An inaccessible or unknown message returns `404`. A disabled channel does not transfer history to a different owner.

```bash CLI theme={null}
inkbox whatsapp get 33333333-3333-4333-8333-333333333333 --identity my-agent
```

## List messages

```text theme={null}
GET /messages
```

| Query parameter | Type | Default | Description |
| :- | :- | :- | :- |
| `agent_identity_id` | UUID | Credential identity | Select the authorized agent |
| `conversation_id` | UUID | None | Limit results to one conversation |
| `limit` | integer | `50` | Page size, 1–200 |
| `offset` | integer | `0` | Number of entries to skip |
| `thread_id` | UUID | None | Filter a quote chain; requires `conversation_id` |
| `is_read` | boolean | None | Filter local read state |
| `is_blocked` | boolean | Credential-dependent | Select blocked visibility only where authorized |

Returns a bare array, newest first, with a stable message-ID tie-break. Keep the same filters when advancing the offset. Message lists show stored state; they do not refresh every message's delivery status.

```bash CLI theme={null}
inkbox whatsapp list --identity my-agent \
  --conversation-id 22222222-2222-4222-8222-222222222222 \
  --limit 50 --offset 0
```

## Message object

| Field | Type | Description |
| :- | :- | :- |
| `id`, `agent_identity_id`, `conversation_id` | UUID | Stable message and ownership identifiers |
| `direction` | string | `inbound` or `outbound` |
| `sender_access` | string or null, optional | Receipt-time admission: `direct` or `sponsored`; not sender trust or command permission |
| `local_number` | string | Original dedicated E.164 number |
| `remote_number` | string or null | Remote peer’s E.164 number for direct messages; null for groups |
| `sender_number` | string or null | Actual author’s E.164 number when available |
| `participants` | string\[] | Known remote E.164 numbers; singleton for direct messages; excludes the local number |
| `is_group` | boolean | Whether this is an existing group conversation |
| `group_name` | string or null | Group display name; not a destination selector |
| `agent_mentioned` | boolean | Whether an incoming message natively mentions this agent; defaults to `false` when absent |
| `mentions` | object\[] | Native mentions in the current message; defaults to `[]`. Each entry has nullable `text`, nullable E.164 `phone_number` (only when available and visible to the agent), and boolean `is_agent` |
| `unresolved_participant_count` | integer | Members whose phone number is unavailable |
| `content` | string or null | Stored text; not a `text` alias |
| `media` | object\[] | [Media metadata](https://inkbox.ai/docs/api/whatsapp/media.md#media-object), including explicit pending/unavailable states |
| `status` | string | Send or receive state listed below |
| `delivery_state` | string or null | `pending`, `delivered`, or `unknown`; null for incoming messages |
| `recipients` | object\[] | Direct recipient’s outbound send/delivery observations; empty for inbound and group messages |
| `reply_to_message_id`, `thread_id`, `thread_root_message_id` | UUID or null | Authorized quote and thread identifiers; null when absent |
| `reactions` | object\[] | Currently applied [reactions](https://inkbox.ai/docs/api/whatsapp/reactions.md), never queued or uncertain receipts |
| `is_read`, `is_blocked` | boolean | Local read state and authorized visibility metadata |
| `error_code`, `error_detail` | string or null | Safe error information, when available |
| `source_at` | timestamp or null | Source message time when known |
| `created_at`, `updated_at` | timestamp | Stored-message timestamps |
| `sent_at`, `delivered_at`, `failed_at` | timestamp or null | Positive observed lifecycle milestones |
| `status_observed_at` | timestamp or null | Freshness of the latest status observation |
| `status_refresh_pending` | boolean | Whether a status refresh remains outstanding |

Each recipient object has `remote_number`, `delivery_status`, `delivery_state`, nullable `error_code` and `error_detail`, and nullable `sent_at`, `delivered_at`, and `failed_at` timestamps. Required response keys remain present when null. Arrays remain arrays, including when empty.

### Send and delivery are separate

| `status` | Meaning |
| :- | :- |
| `received` | Incoming message stored |
| `queued` | Outgoing message accepted |
| `sending` | Sending in progress |
| `sent` | Sending confirmed; delivery may still be pending or unknown |
| `delivered` | Delivery positively confirmed |
| `failed` | Explicit failure recorded |
| `uncertain` | Send outcome itself is unconfirmed; check before sending another copy |
| `cancelled` | Message cancelled before completion |

`status: "sent"` with `delivery_state: "unknown"` is not a failed send. Missing delivery confirmation does not justify automatic resend. A later delivery event can provide new evidence.

### Date filters

Both message and conversation lists accept `start_datetime`, `end_datetime`, and `tz`. Bounds filter `created_at`; start and end instants are inclusive. A bare end date includes the entire calendar day. Zone-less values use the supplied IANA timezone, or UTC by default. Explicit-offset timestamps retain their own timezone. Filtering happens before pagination.

### Quoted-reply fallback policy

`plain_reply_fallback` accepts only a boolean and defaults to `true`. It permits an
ordinary reply only when the selected target is known **before sending** not to
support a native quote. All currently supported WhatsApp message targets support
native quotes, so the current service does not use a plain fallback. This option
does not turn a missing, inaccessible, unsettled, or unavailable quote target into
an ordinary send. A rejected or uncertain quoted send is never retried as plain.
Use `false` to require native quoting explicitly; keep the original request and
idempotency key after uncertainty.

SDK spellings are `plain_reply_fallback` (Python/Rust) and `plainReplyFallback`
(TypeScript). The CLI option is `--no-plain-reply-fallback`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.