Texts
List, search, and manage SMS/MMS text messages for your phone numbers. The text API is conversation-centric: 1:1 SMS/MMS and beta group MMS use the same endpoints and response objects.
All text endpoints are scoped to a phone number: /numbers/{phone_number_id}/texts/...
Path parameters
| Parameter | Type | Description |
|---|---|---|
phone_number_id | UUID | Unique identifier of the phone number |
Send text POST
POST /numbers/{phone_number_id}/textsSend an outbound message from one of your phone numbers. Pass to as a string for 1:1 SMS/MMS, as a list of 1-8 E.164 numbers for a conversation send, or pass conversation_id to reply into an existing conversation. Lists with 2 or more resolved recipients are sent as beta group MMS.
Beta: Group MMS and conversation sends are beta. Some carriers may reject group chats or MMS from 10DLC numbers even when the sender is ready and recipients have opted in.
The legacy 1:1 request shape still works unchanged:
Beta group MMS uses the same endpoint:
To reply into an existing conversation, pass conversation_id instead of to. The server resolves the conversation to its participants and applies the same 1:1 or group routing based on recipient count.
Preconditions
- Sender warm-up. A newly provisioned local number can take around 10-15 minutes for its 10DLC campaign to register downstream. While
sms_statusis"pending", sends return409 sender_sms_pending. - Recipient opt-in. Every recipient must opt in before you can text them. Sending to a recipient who has not opted in returns
403 recipient_not_opted_in; sending to a recipient who later sentSTOPreturns403 recipient_opted_out. - Contact rules. Rules configured on the sending number still apply. Multi-recipient sends are all-or-nothing: one blocked or non-opted-in recipient rejects the request.
- Group destination region. Group MMS recipients must be US or Canadian E.164 numbers.
- Beta carrier behavior. Group MMS and MMS over 10DLC are still carrier-dependent; some carriers may reject group chats or MMS from 10DLC numbers.
Rate limits
A phone number on Inkbox's default 10DLC campaign can send to at most 100 recipients per rolling 24-hour window. A 3-recipient group message counts as 3 recipient sends. Registering your own brand and campaign lifts this cap to the carrier-assigned tier; see 10DLC registration.
When the cap is reached, 429 responses include:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum recipient sends allowed per number per 24 hours (100 on the default campaign) |
X-RateLimit-Remaining | Remaining recipient 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 |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
to | string | string[] | Conditional | One E.164 destination for 1:1, or a list of 1-8 E.164 destinations. Lists with 2 or more recipients send beta group MMS. Mutually exclusive with conversation_id |
conversation_id | UUID | null | Conditional | Existing conversation to reply into. The server resolves it to the conversation participants. Mutually exclusive with to |
text | string | null | No | Message body, up to 1600 characters. Required unless media_urls has at least one item |
media_urls | string[] | null | No | Publicly fetchable media URLs to attach, up to 10 entries. Required unless text is present |
Response (201)
1:1 responses keep the legacy fields populated and add conversation-aware fields:
Group responses keep the same object shape. remote_phone_number and legacy timestamp/error fields are null; use conversation_id, the message-level delivery_status rollup, and recipients[].
Group outbound — remote_phone_number is null (no single remote party), top-level delivery_status is the message-level rollup, legacy timestamp/error fields stay null, and recipients carries one entry per addressee:
The final delivery state of every leg is reported asynchronously through text.* webhooks. For group sends, lifecycle events fire once per recipient with data.recipient_phone_number identifying the leg.
Error responses
| Status | Description |
|---|---|
| 400 | Phone number not active |
| 403 | recipient_not_opted_in - one or more recipients have not texted START to a number in your organization |
| 403 | recipient_opted_out - one or more recipients texted STOP and can no longer be messaged |
| 403 | Send blocked by a contact rule on the sending number |
| 404 | Phone number or conversation_id not found |
| 409 | sender_sms_pending - local number's 10DLC registration is still propagating |
| 409 | Sending number is not eligible to send SMS/MMS |
| 422 | Invalid request body, no text/media content, both/neither to and conversation_id, duplicate recipients, too many recipients, or unsupported group destination |
| 429 | 24-hour recipient-send limit reached on this number; respect the Retry-After header |
List texts GET
GET /numbers/{phone_number_id}/textsList text messages for a phone number, newest first. Group messages appear alongside 1:1 messages.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
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 returns only non-blocked rows, and omitting it applies the caller's default visibility |
Identity-scoped API keys never see contact-rule-blocked texts, regardless of is_blocked. Admin API keys and human sessions see blocked and non-blocked texts by default; use is_blocked=true for an admin-side blocked listing.
Response (200)
Error responses
| Status | Description |
|---|---|
| 403 | Phone number belongs to a different organization |
| 404 | Phone number not found |
| 422 | Validation failed: missing both text and media_urls, to list outside 1–8 recipients, group send to a non-US/CA number, media_urls longer than 10, or text longer than 1600 characters |
Get text GET
GET /numbers/{phone_number_id}/texts/{text_id}Get a single text message by ID.
Path parameters
| Parameter | Type | Description |
|---|---|---|
text_id | UUID | Unique identifier of the text message |
Response (200)
Returns a TextMessage object.
MMS messages have type: "mms" and a media array. Each media item includes a content_type, size in bytes, and a URL. URLs returned for stored inbound MMS media are presigned and expire after 1 hour.
Error responses
| Status | Description |
|---|---|
| 404 | Text not found, wrong phone number, or wrong organization |
Update text PATCH
PATCH /numbers/{phone_number_id}/texts/{text_id}Update a text message, for example to mark it as read.
Path parameters
| Parameter | Type | Description |
|---|---|---|
text_id | UUID | Unique identifier of the text message |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
is_read | boolean | No | Mark as read (true) or unread (false) |
Request example
Response (200)
Returns the updated TextMessage object.
Error responses
| Status | Description |
|---|---|
| 404 | Text not found, wrong phone number, or wrong organization |
Search texts GET
GET /numbers/{phone_number_id}/texts/searchFull-text search across text message bodies for a phone number. Results are ranked by relevance.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
q | string | - | Search query (required, 1-500 characters) |
limit | integer | 50 | Number of results (1-200) |
is_blocked | boolean | - | Filter by blocked state. true searches only blocked rows, false searches only non-blocked rows, and omitting it applies the caller's default visibility |
Identity-scoped API keys never see contact-rule-blocked texts in search results. Admin API keys and human sessions see everything by default; pass is_blocked=false to exclude blocked spam from search results or is_blocked=true to search only blocked rows.
Response (200)
Returns a list[TextMessage] matching the query.
Error responses
| Status | Description |
|---|---|
| 403 | Phone number belongs to a different organization |
| 404 | Phone number not found |
| 422 | Missing or invalid q parameter |
List conversations GET
GET /numbers/{phone_number_id}/texts/conversationsList conversation summaries ordered by most recent message, with latest-message preview and unread count. By default this endpoint returns only 1:1 conversations so existing clients keep the same sidebar behavior. Pass include_groups=true to include group conversations.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of results (1-200) |
offset | integer | 0 | Pagination offset |
include_groups | boolean | false | Include group conversations in the listing |
is_blocked | boolean | - | Filter by blocked state of messages. true summarizes blocked rows only, false summarizes non-blocked rows only, and omitting it applies the caller's default visibility |
With is_blocked=false, latest previews, ordering, unread counts, and totals are computed from non-blocked rows only, so blocked-only conversations drop out. Identity-scoped API keys never see blocked rows in conversation summaries.
Response (200)
Each row carries the conversation's id, the participant list, and an is_group flag. For 1:1 rows, remote_phone_number is the single participant; for group rows it's null (the participant set is the source of truth). latest_has_media is the reliable signal that the latest message carried media — derived from the message's media column, since carriers tag many text-only messages as MMS at the wire.
Error responses
| Status | Description |
|---|---|
| 403 | Phone number belongs to a different organization |
| 404 | Phone number not found |
Get conversation GET
GET /numbers/{phone_number_id}/texts/conversations/{key}Get all messages in a conversation, newest first. {key} accepts the conversation UUID for any conversation. For 1:1 threads, the key can also be the remote phone number, short code, or sender ID.
Group conversations must be addressed by UUID. Use the id from the conversation summary or conversation_id from any message in the group.
Path parameters
| Parameter | Type | Description |
|---|---|---|
key | string | Conversation UUID, or a 1:1 remote phone number / short code / sender ID |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of results (1-200) |
offset | integer | 0 | Pagination offset |
Identity-scoped API keys see only non-blocked rows in the thread. Admin API keys and human sessions see blocked and non-blocked rows merged inline, with each row tagged by is_blocked.
Response (200)
Returns a list[TextMessage] for the conversation.
Error responses
| Status | Description |
|---|---|
| 400 | Conversation key is malformed (not a UUID and not a recognized phone number) |
| 403 | Phone number belongs to a different organization |
| 404 | Phone number not found, or no conversation matches the key on this phone number |
Update conversation PATCH
PATCH /numbers/{phone_number_id}/texts/conversations/{key}Bulk-update the read state for all non-blocked messages in a conversation.
Path parameters
| Parameter | Type | Description |
|---|---|---|
key | string | Conversation UUID, or a 1:1 remote phone number / short code / sender ID |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
is_read | boolean | Yes | Mark all messages as read (true) or unread (false) |
Request example
Response (200)
For 1:1 conversations, remote_phone_number is populated for legacy callers. For group conversations, it is null and conversation_id is the stable key. updated_count: 0 means no messages needed updating.
Error responses
| Status | Description |
|---|---|
| 400 | Conversation key is malformed (not a UUID and not a recognized phone number) |
| 403 | Phone number belongs to a different organization |
| 404 | Phone number not found, or no conversation matches the key on this phone number |
| 422 | Missing or invalid is_read |
Text message object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Message ID |
direction | string | "inbound" or "outbound" |
local_phone_number | string | Your Inkbox number (E.164) |
remote_phone_number | string | null | Legacy 1:1 remote party. Populated for 1:1 and inbound messages; null for outbound group messages |
text | string | null | Message body. Null for MMS-only messages |
type | string | "sms" or "mms". Group sends are always "mms" |
media | array | null | Media attachments (MMS only). Each item has content_type, size, and url |
is_read | boolean | Whether the message has been read |
delivery_status | string | null | For outbound messages: "queued", "sent", "delivered", "delivery_failed", "delivery_unconfirmed", or "sending_failed". Null for inbound. On groups this is a rollup across recipients[] (the lowest lifecycle rank present; "delivered" is preferred at the terminal tier on mixed-outcome ties) — treat recipients[].delivery_status as authoritative per-leg |
origin | string | How the message was created. "user_initiated" for messages sent via the API |
error_code | string | null | Legacy 1:1 delivery error code. Null for inbound and group messages; use recipients[].error_code for group |
error_detail | string | null | Legacy 1:1 delivery error detail. Null for inbound and group messages; use recipients[].error_detail for group |
sent_at | string (ISO 8601) | null | Legacy 1:1 handoff timestamp. Null for inbound and group messages; use recipients[].sent_at for group |
delivered_at | string (ISO 8601) | null | Legacy 1:1 confirmed-delivery timestamp. Null for inbound and group messages; use recipients[].delivered_at for group |
failed_at | string (ISO 8601) | null | Legacy 1:1 failure timestamp. Null for inbound and group messages; use recipients[].failed_at for group |
conversation_id | string (UUID) | null | Stable conversation key for 1:1 and group threads |
sender_phone_number | string | null | Sender for inbound messages. Null for outbound messages because the sender is local_phone_number |
recipients | array | null | Per-recipient delivery state for outbound messages. One entry for 1:1; 2-8 entries for group; null for inbound |
is_blocked | boolean | Whether the message was rejected by a contact rule or by default-block in whitelist mode. Identity-scoped API keys never receive rows where this is true |
created_at | string (ISO 8601) | Creation timestamp |
updated_at | string (ISO 8601) | Last update timestamp |
Recipient object
recipients[] is only present on outbound text messages.
| Field | Type | Description |
|---|---|---|
recipient_phone_number | string | Recipient phone number in E.164 format |
delivery_status | string | null | Per-recipient delivery status |
carrier | string | null | Carrier reported for the recipient, when available |
line_type | string | null | Carrier-reported line type, when available |
error_code | string | null | Carrier or Inkbox error code for this recipient |
error_detail | string | null | Human-readable detail accompanying error_code |
sent_at | string (ISO 8601) | null | When this recipient was handed off for delivery |
delivered_at | string (ISO 8601) | null | When delivery to this recipient was confirmed |
failed_at | string (ISO 8601) | null | When delivery to this recipient failed |
Conversation summary object
| Field | Type | Description |
|---|---|---|
remote_phone_number | string | null | Legacy 1:1 participant. Null for group conversations |
id | string (UUID) | null | Conversation UUID. Use this as the canonical key for group conversations |
participants | string[] | null | Remote participants in the conversation |
is_group | boolean | null | Whether the conversation has more than one remote participant |
latest_text | string | null | Latest message body preview |
latest_direction | string | Direction of the latest message |
latest_type | string | "sms" or "mms" for the latest message |
latest_has_media | boolean | Whether the latest message actually has media attachments. This is separate from latest_type because group and carrier-routed messages can be "mms" without attachments |
latest_message_at | string (ISO 8601) | Latest message timestamp |
unread_count | integer | Unread message count in the summarized view |
total_count | integer | Total message count in the summarized view |