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
Send 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 |
Dedicated outbound lines carry an additional guardrail on agent-initiated conversations. A dedicated outbound line can start conversations with at most 40 new recipients per rolling 24-hour window, which keeps the line in good standing. It applies only to recipients the line hasn't reached before:
- It does not apply to a recipient the agent has already connected with on that line.
- It does not apply when the recipient messages the line first.
Repeat sends to an already-connected recipient never count against it. In a group chat, each previously unknown participant counts as one new recipient, and the whole request is rejected if the line lacks enough remaining slots. At the cap, sends return 429 with error: "outbound_line_new_contact_rate_limited" and the same Retry-After and X-RateLimit-* headers, where Retry-After counts down until the oldest new recipient in the window ages out.
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 |
The 404 and 409 connection errors carry a structured detail — read detail.error to branch and detail.message for the sentence to relay:
detail.error | Meaning | Recovery |
|---|---|---|
imessage_no_assignment | The recipient has never connected to this agent | Relay the connect command and router number named in detail.message, or the dedicated line to message first |
imessage_awaiting_inbound | An assignment exists, but the recipient has not sent the first message | Wait for the recipient to open the conversation; only dedicated outbound lines may initiate |
imessage_assignment_inactive | The recipient disconnected, so the old conversation is history | Have them reconnect, or start a new conversation on a dedicated outbound line |
Reactions, read receipts, and typing indicators return imessage_assignment_inactive the same way when the conversation's assignment is no longer active.
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
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 |
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
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)
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 |