Messages
One endpoint sends everything. Address a recipient with to + from_handle, or send into an existing thread with conversation_id. Whether the recipient gets an iMessage or an SMS is decided per recipient automatically; WhatsApp is opted into per message with channel.
Send a message
POST/v1/messages
curl -X POST https://api.msgbubbles.com/v1/messages \
-H "Authorization: Bearer sk_live_…" \
-H "content-type: application/json" \
-H "Idempotency-Key: order-1042-confirmation" \
-d '{
"to": "+15555550123",
"from_handle": "+18005551111",
"text": "your order shipped!"
}'Body parameters
| Field | Type | Notes |
|---|---|---|
to | string | Recipient — E.164 phone number or iMessage email. Required unless conversation_id is set. |
from_handle | string | One of your numbers (see GET /v1/numbers). Required unless conversation_id is set. |
conversation_id | uuid | Send into an existing thread instead of to + from_handle. Required for group chats. |
text | string | Message body. text or attachment_ids (or both) must be present. |
attachment_ids | uuid[] | Up to 10 staged attachments — see Attachments. |
reply_to | uuid | Send as a threaded reply to one of your message ids. |
effect | string | iMessage bubble/screen effect id (e.g. confetti). |
channel | "whatsapp" | Force the WhatsApp channel. Needed when the from number serves both iMessage/SMS and WhatsApp — iMessage/SMS wins by default. |
voice_memo | boolean | Send audio attachments as native voice-note bubbles instead of file attachments. |
Idempotency
Pass an Idempotency-Key header to make retries safe. A repeated key returns the original message with 200 instead of sending twice; a first send returns 202.
The message object
{
"data": {
"id": "0d4b1f3a-…",
"conversation_id": "7f2c9e1b-…",
"direction": "outbound",
"status": "queued",
"channel": null,
"text": "your order shipped!",
"effect": null,
"reply_to": null,
"error": null,
"metadata": null,
"attachments": [],
"sent_at": null,
"delivered_at": null,
"read_at": null,
"created_at": "2026-06-11T18:21:04.000Z"
},
"request_id": "req_…"
}Message lifecycle
queued → sent → delivered → read, with failed as the terminal error state and unsent after a successful unsend. Each transition also fires a webhook, so you rarely need to poll. channel resolves once the message is sent — imessage, sms, or whatsapp.
List and fetch
GET/v1/messages
Paginates newest-first. Query params: conversation_id (filter to a thread), limit (1–100, default 50), and before (ISO timestamp cursor). The response is { data, has_more, request_id } — pass the oldest created_at as the next before.
GET/v1/messages/:id
Check reachability
GET/v1/handles/availability
Look up whether a recipient is reachable on iMessage — whether a send will go blue (iMessage) or fall back to green (SMS) — before you send. Pass the recipient as a handle query param (E.164 number or iMessage email).
curl https://api.msgbubbles.com/v1/handles/availability?handle=+15555550123 \
-H "Authorization: Bearer sk_live_…"{
"data": { "handle": "+15555550123", "imessage": true, "channel": "imessage" },
"request_id": "req_…"
}imessage is whether the handle is registered with iMessage; channel is the channel a send would use (imessage or sms). "Not on iMessage" usually means Android, but also de-registered or never-activated Apple numbers — we can only see iMessage registration.
Actions on a sent message
React
POST/v1/messages/:id/react
{ "reaction": "love" } // tapbacks: love, like, dislike, laugh, emphasize, question
{ "reaction": "🔥" } // or any single emoji
{ "reaction": "love", "remove": true }Reactions target a delivered message (409 before then) and work on iMessage and WhatsApp.
Edit and unsend (iMessage only)
POST/v1/messages/:id/edit
POST/v1/messages/:id/unsend
Body for edit: { "text": "updated body" }. Both return 409 for SMS — carriers offer no edit or recall — and before the message has actually sent.
Retry a failed send
POST/v1/messages/:id/retry
Re-queues a message in status failed (for example, sent while the number was offline past the retry window). Parts that already went out are never re-sent.