Skip to main content
POST

Send a message

Method: POST | Endpoint: /v1/messages Description: Send a message to one contact (by contact_id or phone number), into an existing iMessage group (chat_guid), or to a new group (participant_phones). One-to-one messages can carry files by URL; see Send attachments. The call returns at once; delivery progress arrives as webhook events, or poll Retrieve a message. Authentication: API key in the Authorization header: Authorization: Bearer ib_pk_<64 hex>. Create keys in the dashboard under Settings → API access.

Request Body

Fields not listed above are ignored without an error: the request still succeeds, and they are not stored. reply_to_message_id and effect only show on iMessage. If the message goes out as SMS (RCS included), it is sent as plain text without the quote or the effect, and the API doesn’t refuse it. channel on Retrieve a message tells you which way it went. Neither can be combined with attachments: that request is refused with 422 validation_error. Both fields count toward the idempotency payload when set. attachments count toward the idempotency payload too, in order. A replay never downloads them again. A request whose files can’t be downloaded or don’t meet the limits is refused with a 422 and creates no message; see Send attachments.

Responses

202 Accepted
status is pending or queued. A message held by a capacity limit or an offline line is still accepted with queued and retried automatically. A message waiting for the contact’s working hours or the line’s hourly limit for new contacts is pending, with held_until and held_reason set. For a group send, to is null and conversation_kind is "group"; a new group’s chat_guid is null until the group is created a few seconds later. effect and reply_to_message_id echo what you sent, or null. attachments lists the files the message carries, in your order, as kind (image, gif, video, audio or file), mime_type and size_bytes; it’s [] for a text message. direction is always outbound here, and created_at is when iBubbl recorded the message; it never changes, and it’s what List messages sorts and filters by. 401 Unauthorized
Other message values: Authorization Bearer token required, API key required (use Authorization: Bearer ib_pk_…), Invalid API key format, Customer account not found. 403 Forbidden
Your iBubbl account is suspended or cancelled (message is then Account is cancelled). Every API-key endpoint returns this. 404 Not Found
Also returned for a contact_id that is malformed, belongs to another account, or was deleted; send with to to reach the number again (that creates a new contact). Other error value: group_not_found (no group with that chat_guid on your account). 409 Conflict
422 Unprocessable Entity
Other error values: contact_opted_out, contact_blocked, no_eligible_line, unresponsive_contact, cold_cap_exhausted, daily_cap_exhausted, followup_cap_exhausted, group_outbound_blocked_suppression (with a blocked_phones list), insufficient_participants, reply_target_not_found, reply_target_unsent, reply_target_partly_unsent, and for attachments first_message_needs_text, attachment_fetch_failed, attachment_too_large, unsupported_attachment and attachment_combination_not_supported. See Errors. 429 Too Many Requests
Sends with attachments only, and rarely: iBubbl is already downloading as many attachment sends as it can. The Retry-After header says how many seconds to wait (5); then send the same request with the same idempotency_key. Text-only sends never get a 429. 500 Internal Server Error
Other error value: internal_error. Retry with the same idempotency_key.

Authorizations

Authorization
string
header
required

Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.

Body

application/json

Fields not listed here are ignored without an error: the request still succeeds and they are not stored.

idempotency_key
string
required

Client-supplied unique key per logical send. Keys never expire: reusing a key with the same payload replays the original message, however long ago it was sent, and the same key with a different payload returns 409 idempotency_conflict. The one exception is a message that ended failed or expired: its key is released and the retry is sent as a new message. The payload compared is the routing field exactly as sent (to, contact_id, chat_guid, or the sorted participant_phones plus group_name), body, and effect, reply_to_message_id and attachments when set, so a retry that switches from to to the same contact's contact_id is a 409. Generate a new UUIDv4 for every distinct message.

Required string length: 1 - 128
Example:

"a3f2c8d9-e0f1-4b4d-9c5a-7b8e2d1f0c4a"

contact_id
string | null

1:1 send by existing contact ID (a UUID). Mutually exclusive with to, chat_guid, and participant_phones.

Example:

"c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"

to
string | null

1:1 send by E.164 phone number (e.g. +14155551234). If the number doesn't match an existing contact, one is created automatically. Mutually exclusive with contact_id, chat_guid, and participant_phones.

Pattern: ^\+[1-9]\d{7,14}$
Example:

"+14155551234"

chat_guid
string | null

Send into an existing iMessage group identified by its opaque chat_guid (e.g. any;+;<hex>). Webhook events don't carry it: read it from the response to a send into the group, or from GET /v1/messages/{id} for any message in the group (an inbound message_id works too). The value changes when a participant is added to the group; an old value then returns 404 group_not_found. Mutually exclusive with contact_id, to, and participant_phones.

Example:

"any;+;067ace6caf6c43aaa2f7d643975e15ea"

participant_phones
string[] | null

Create a brand-new iMessage group and send the first message in one call. List of 2 or more E.164 phone numbers (your iBubbl line is added automatically; do not include it). Mutually exclusive with contact_id, to, and chat_guid. Optionally pair with group_name to set the group's display name.

Example:
group_name
string | null

Display name for a new iMessage group. Only valid alongside participant_phones. Optional — iMessage will fall back to the participants' names if omitted.

Maximum string length: 80
Example:

"Team Alpha"

body
string | null

The message text. Plain text only; emoji + Unicode are supported. For a line break, include a newline (\n in the JSON string); use \n, not \r\n. A retry with the same idempotency_key must send the identical body, line breaks included, or it returns 409 idempotency_conflict. Required unless attachments is set; with attachments it is sent as their caption, and it is required for the first message to a contact (422 first_message_needs_text).

Minimum string length: 1
Example:

"Hey Sam — quick check-in about your order."

attachments
string<uri>[] | null

Up to 10 files to send, each an https URL iBubbl downloads when the request arrives. Images (JPEG, PNG, WebP, HEIC, HEIF) and GIFs up to 25 MiB each, MP4/MOV video up to 50 MiB, PDF up to 10 MB, audio (MP3, M4A, AAC) up to 25 MiB on its own; 86 MB in all. With other files, audio must be one MP3 of at most 2 MB. The type comes from the response's Content-Type (or the URL's extension when that is application/octet-stream). A URL that can't be downloaded, or a file that breaks these rules, rejects the whole request (422 attachment_fetch_failed, attachment_too_large, unsupported_attachment, attachment_combination_not_supported) and nothing is sent. Only with to or contact_id, and not with effect or reply_to_message_id. The URLs, in order, are part of the payload compared for idempotency; a replay never downloads them again.

Required array length: 1 - 10 elements
Required string length: 1 - 2083
Example:
reply_to_message_id
string | null

Send as an inline reply that quotes an earlier message (a UUID: the id of a message, or the message_id of a message.inbound event). The message must be in the conversation you are sending to: the same contact for a 1:1 send, the same group for a chat_guid send; otherwise 422 reply_target_not_found. A message the contact unsent returns 422 reply_target_unsent, and one they unsent part of 422 reply_target_partly_unsent. iMessage only: when the message goes out as SMS (see channel) it is sent as plain text, without the quote. Not allowed with participant_phones or attachments.

Example:

"8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"

effect
enum<string> | null

An iMessage send effect. Bubble effects: slam, loud, gentle, invisible (Invisible Ink). Screen effects: echo, spotlight, balloons, confetti, love, lasers, fireworks, celebration, shooting_star. iMessage only: when the message goes out as SMS (see channel) it is sent without the effect. Not allowed with participant_phones or attachments.

Available options:
slam,
loud,
gentle,
invisible,
echo,
spotlight,
balloons,
confetti,
love,
lasers,
fireworks,
celebration,
shooting_star
Example:

"confetti"

Response

Message accepted for delivery.

id
string
required

Unique message identifier (UUID).

direction
enum<string>
required

outbound = sent from your line (through the API, the dashboard or an automation); inbound = received from a contact. An inbound message has status delivered, or read once it has been read in the dashboard.

Available options:
inbound,
outbound
created_at
string
required

ISO 8601 UTC timestamp, with microseconds and a Z, of when iBubbl recorded the message. It never changes, so it is what GET /v1/messages sorts and filters by; pass it back as created_after / created_before unchanged.

Example:

"2026-05-17T14:23:45.123456Z"

status
enum<string>
required

Lifecycle state. pending = accepted and on its way out; it may be waiting for the contact's working hours or the line's hourly limit for new contacts (see held_until). queued = parked by a capacity hold (hourly_cap_hit / line_offline), retried automatically. sent / delivered / read = delivery progression. failed = the send failed — see webhook message.failed for the reason; it can follow sent (or a read with no confirmed delivery) when iMessage reports the send failed after accepting it (failed_reason imessage_undeliverable / imessage_error:<code>), and such a message can still move to delivered / read if Apple later confirms it. expired = still queued after an hour, so dropped; it will not send. cancelled = stopped in the dashboard before it was sent; it will not send (webhook message.cancelled).

Available options:
queued,
pending,
sent,
delivered,
read,
failed,
expired,
cancelled
body
string
required

The message text as sent.

attachments
MessageAttachment · object[]
required

The files on this message, in the order they were sent: sent with attachments, from the dashboard or an automation, or received from the contact. Empty when there are none. File contents and URLs are not returned.

to
string | null

The contact's phone number (E.164) for 1:1 messages: the destination of an outbound message, and the sender of an inbound one. Null for group messages — the group is identified by chat_guid instead.

Example:

"+14155551234"

contact_id
string | null

ID of the contact this message was sent to. For groups, this is the 'primary' participant the message was addressed to (the first participant in participant_phones on a new group create); the conversation's full participant list is available via the conversation.

conversation_id
string | null

ID of the conversation this message belongs to. Stable for the lifetime of the conversation; use it to fetch the thread or correlate webhook events.

conversation_kind
enum<string>
default:one_to_one

one_to_one for sends to a single contact, group for iMessage groups (2+ participants in addition to your iBubbl line).

Available options:
one_to_one,
group
chat_guid
string | null

Opaque identifier for the group chat (e.g. any;+;<hex>). Returned for group messages — use it to address later messages to the same group via the chat_guid request shape. Null for one_to_one messages, and for a brand-new group until the group has been created in iMessage (that happens when the first message goes out, which can wait for sending hours); poll GET /v1/messages/{id} to see it fill in. It changes when a participant is added to the group.

Example:

"any;+;067ace6caf6c43aaa2f7d643975e15ea"

channel
enum<string> | null

How the message travelled: imessage, or sms (a text, including RCS). For an outbound message, how it actually went out (sms when the recipient couldn't be reached on iMessage); null until it has been sent. For an inbound message, how it arrived; null when that wasn't recorded.

Available options:
imessage,
sms
sent_at
string | null

ISO 8601 UTC timestamp. Set when the message is accepted, then replaced by the actual send time once it goes out. Use status (sent / delivered / read), not sent_at, to tell whether it has actually gone out.

Example:

"2026-05-17T14:23:45.123Z"

held_until
string | null

While a pending message is being held, the expected time (ISO 8601 UTC) it will go out: the next opening of the contact's working hours; for hourly_limit, an estimate of when a slot frees. Recomputed within about 10 seconds. Null when the message is not held, when held_reason is working_hours but no sending day is enabled, and when it is hourly_limit with no estimate.

Example:

"2026-10-08T16:00:00+00:00"

held_reason
enum<string> | null

Why a pending message is being held right now: working_hours (outside the contact's working hours) or hourly_limit (the line has started its limit of new contacts for this hour; the message goes out by itself when a slot frees, at about held_until). Null when it is not held.

Available options:
working_hours,
hourly_limit
effect
enum<string> | null

The iMessage send effect requested for this message, or null. It shows on the recipient's device only when channel is imessage.

Available options:
slam,
loud,
gentle,
invisible,
echo,
spotlight,
balloons,
confetti,
love,
lasers,
fireworks,
celebration,
shooting_star
reply_to_message_id
string | null

The ID of the message this one quotes as an inline reply, or null. The quote shows on the recipient's device only when channel is imessage.