Messages
Send and list 1:1 or group iMessages. iMessage requests are scoped to the agent identity and keyed by conversation_id; callers never choose a sending number in the message request. Shared lines remain hidden, while an attached dedicated number determines whether the identity can start a conversation or group.
Send message POST
POST /messagesSend an outbound iMessage. Pass conversation_id to reply into an existing conversation, or to as either one E.164 phone number or a list of 1-8 E.164 phone numbers — exactly one of the two.
Shared and dedicated inbound numbers can reply only after the recipient has started a conversation. A dedicated outbound number can start a new 1:1 conversation or a group chat; lists with 2-8 recipients select group behavior, while a scalar or one-item list keeps the 1:1 behavior.
Reply into a conversation:
Or address the connected recipient directly:
Or start or select a group from a dedicated outbound number:
For groups, an unresolved local conversation with the same requested members is reused before a remote thread has been established. An established best-known match is reused only when its current membership can be verified and still matches the request. If no reusable match remains, a new group is created; multiple best-known matches return 409. A different to set never edits an existing group's membership. Keep the returned conversation_id and use it to continue the exact same thread. See Group chats for membership and delivery semantics.
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, which always send as their own identity |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string | string[] | Conditional | One E.164 phone number, or 1-8 distinct E.164 phone numbers. A list with 2 or more recipients starts or selects a group. 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,995 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 to turn raw bytes into a URL. Required unless text is present |
send_style | string | null | No | Expressive send style applied to a 1:1 or group message, including group creation and conversation replies. Can accompany text, media, or both |
Preconditions
- Identity opt-in. The sending identity must have
imessage_enabled: true; otherwise sends return400. - Number capability. Shared and dedicated inbound numbers require recipient-first contact. A dedicated outbound number can start 1:1 conversations and is required for groups.
- Group participants. Group
tolists contain 2-8 distinct remote E.164 phone numbers and cannot include the attached sending line. Every participant must pass opt-out and contact-rule checks before the message is queued. - Connection still active. If the recipient disconnected from the agent, sends into the old conversation return
409until they reconnect through the router. - Contact rules. Sends to a recipient blocked by the identity's contact rules return
403before 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 (imessage.sent, imessage.delivered, imessage.delivery_failed) to track it without polling.
Error responses
| Status | Description |
|---|---|
| 400 | Sending identity is not enabled for iMessage, or agent_identity_id is missing on an admin send by to |
| 402 | The organization's current billing-cycle iMessage capacity is exhausted |
| 403 | A recipient is blocked by a contact rule or has opted out |
| 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, has disconnected, a group send lacks a dedicated outbound number, multiple groups match, current group membership is inconsistent or changes while resolving, or the line attachment changed |
| 422 | Invalid body: both/neither to and conversation_id, invalid or duplicate recipients, no text or media, more than one media_urls entry, or text over 18,995 characters |
| 429 | 24-hour per-identity send limit or dedicated-line new-contact limit reached; respect the Retry-After header |
| 503 | group_membership_unavailable because current membership for an existing group could not be verified; retry with backoff |
Delivery happens asynchronously after the 201, so delivery failures never surface as send-request errors. Re-read the message to observe every outcome; delivery-lifecycle transitions also emit webhooks.
The first message in a new group also creates the group asynchronously. Check the conversation's group_creation_status: "creating" while that attempt resolves, "ready" after success, or "not_created" after a definitive failure. During creating, the local conversation and pending messages exist before the remote thread does; another send to the same conversation waits behind the elected creation send. A failed first message remains in the same conversation's history. Send another message using that conversation_id to retry group creation; a successful retry binds that same local conversation for later sends. Existing-group membership is rechecked before delivery, so a change after the 201 can instead move the queued message to status: "error". If you still intend to continue that native thread, a later conversation_id retry resolves its current membership again.
List messages GET
GET /messagesList 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 |
include_groups | boolean | false | Include group messages in list results. A known group conversation_id is read directly even when this remains false |
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 |
start_datetime | string | - | Only messages created on or after this date/time |
end_datetime | string | - | Only messages created on or before this date/time; a bare date includes the whole day |
tz | string | UTC | IANA zone for interpreting zone-less start_datetime/end_datetime values. Defaults to UTC |
Identity-scoped API keys never see contact-rule-blocked messages, regardless of is_blocked. Admin API keys and the Inkbox Console see blocked and non-blocked messages by default; use is_blocked=true for a blocked-only audit listing.
start_datetime, end_datetime, and tz filter on created_at. Both bounds are optional and non-breaking — omit them for unchanged behavior. See Filtering by date.
Response (200)
Returns a list[IMessage]. Live tapbacks ride along on each message — see the Message object.
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
POST /mediaUpload 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)
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 1:1 or group send to apply one of Apple's expressive effects. Dedicated-outbound groups support styles on both creation and conversation_id replies, including messages that also carry the single allowed media item:
| 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) | null | The 1:1 connection carrying the conversation; null for group messages |
direction | string | "inbound" or "outbound" |
remote_number | string | null | The human's E.164 phone number for 1:1 messages. On inbound group messages it mirrors sender_number; null on outbound group messages |
sender_number | string | null | Sender's E.164 phone number on an inbound group message; null for 1:1 and outbound group messages |
participants | string[] | null | Best-known E.164 participant snapshot for a group message; null for 1:1 messages |
is_group | boolean | Whether the message belongs to an iMessage group chat |
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 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 |
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, including one entry per group participant; 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 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 |