MessagesSend Messages

Send Messages

Send iMessages with automatic SMS fallback via POST /v1/messages.

Basic Message

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": {
      "text": "Hello!"
    }
  }'

The to field accepts phone numbers in E.164 format (e.g., +14155551234) or iMessage email addresses (e.g., user@example.com).

Sending to a Chat (chatId)

To send into an existing chat (group or 1:1), pass chatId instead of to. The chatId value is the chat.chatId UUID returned from POST /v1/chats/groups or GET /v1/chats/:chatId.

  • to and chatId are mutually exclusive — provide exactly one. Supplying both, or neither, returns 400 INVALID_REQUEST.
  • The channel and sender instance are pinned to the chat; routing.preference and routing.fallback are ignored.
  • If from is supplied, it must resolve to the same instance the chat is pinned to. A mismatch returns 400 FROM_INSTANCE_CANNOT_REACH.
  • An unknown or foreign chatId returns 404 CONVERSATION_NOT_FOUND.
  • Group send on SMS is not supported; returns 400 GROUP_SMS_UNSUPPORTED.
  • createContact is ignored (no single recipient).
curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chatId": "550e8400-e29b-41d4-a716-446655440000",
    "content": { "text": "Hey team" }
  }'

Response (202 Accepted):

{
  "success": true,
  "data": {
    "id": "msg_abc123",
    "status": "queued",
    "conversationId": "550e8400-e29b-41d4-a716-446655440000",
    "from": "+14155551234",
    "createdAt": "2026-05-25T10:00:00.000Z"
  },
  "requestId": "req_abc123"
}

When the send is keyed by chatId, the 202 response carries conversationId (the same UUID) in place of to. The matching message.* lifecycle webhooks for this send carry the full chat fragment — see Chat and sender fragments.

Sender Address (from)

By default, messages are sent from your customer’s default authorized address. You can specify a different authorized address with the from field:

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "from": "+19876543210",
    "content": { "text": "Hello from my other number!" }
  }'

The from address must be in your customer’s authorized address list. Using an unauthorized address returns 403 ADDRESS_NOT_AUTHORIZED. See Authentication for details.

With Effects (iMessage Only)

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": { "text": "Congratulations!" },
    "effect": "confetti"
  }'

See Message Effects for all available effects.

Reply to a Message

Create a threaded reply using the replyTo field (iMessage and WhatsApp):

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": { "text": "Thanks for letting me know!" },
    "replyTo": "msg_550e8400-e29b-41d4-a716-446655440000"
  }'

Attachments vs Media URLs — Which Field Do I Use?

TextBubbles exposes two ways to send non-text content on a message, and they map to different channels. Picking the wrong one is the most common source of “my image didn’t send” bugs, so here is the decision rule.

Text is optional when you send media. Every request must include at least one of content.text (non-empty), content.mediaUrls, or attachments[]. You can send an attachment-only message — voice note, image with no caption, etc. — by omitting content.text or passing an empty string. Sending a request with no text, no mediaUrls, and no attachments[] returns 400.

UseFieldChannelsSource typesNotes
Send a file (photo, video, PDF, audio, any MIME type)attachments[]iMessage and SMS/MMStype: "url" (HTTPS) or type: "base64"Required for non-image files and for base64 payloads. On iMessage, 2+ items arrive as a grouped photo gallery delivered after the text bubble. On SMS, each attachment is delivered as a separate MMS; carrier delivery of non-image MIME types is not guaranteed.
Send an image (or 2+ images as a carousel)content.mediaUrlsiMessage and SMS/MMSHTTPS image URLs onlyOn iMessage, 2+ URLs arrive as a single grouped carousel bubble with the text caption inlined. On SMS, the carrier fetches each URL and attaches it as MMS.

Decision tree

  1. Do you need a non-image file (PDF, video, audio, vCard, etc.) or to send raw bytes (type: "base64")? Use attachments[]. SMS/MMS cannot carry arbitrary file types in all carrier networks — check capabilities first.
  2. Do you want 2+ images to arrive as a single grouped bubble with the text caption inline? Use content.mediaUrls. Multiple attachments[] also arrives as a grouped gallery on iMessage, but the text is sent as a separate bubble before the gallery.
  3. Everything else (single image with an HTTPS URL): either works; prefer attachments[] for explicit control over mimeType and filename.

Do not combine

Do not set both attachments[] and content.mediaUrls on the same request. If you do, attachments[] is sent on the iMessage path and mediaUrls is sent on the SMS path, which produces different payloads depending on which channel the router picks — almost never what you want.

With Attachments

URL attachment (iMessage):

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": { "text": "Check out this image" },
    "attachments": [
      {
        "type": "url",
        "url": "https://example.com/photo.jpg",
        "mimeType": "image/jpeg",
        "filename": "photo.jpg"
      }
    ]
  }'

Media URL (SMS/MMS):

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": {
      "text": "Check out this image",
      "mediaUrls": ["https://example.com/photo.jpg"]
    },
    "routing": { "preference": ["sms"] }
  }'

Voice Memos

To send an audio file as a native voice-memo bubble (inline player with waveform) instead of a generic file attachment, set isAudioMessage: true on the attachment. Voice memos are attachment-only — either omit content.text (send "content": {}) or pass "text": "". Both are accepted. Supported on iMessage and WhatsApp.

Preferred audio formats by channel (sent as-is):

ChannelPreferred format
iMessageMP3 or Opus-in-CAF
WhatsAppOpus-in-OGG

Any other audio format is transcoded server-side to the channel’s preferred format before send.

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": {},
    "attachments": [
      {
        "type": "url",
        "url": "https://example.com/voicenote.caf",
        "mimeType": "audio/x-caf",
        "filename": "voicenote.caf",
        "isAudioMessage": true
      }
    ]
  }'

Without isAudioMessage: true, the audio file arrives as a generic file attachment that the recipient must tap to download.

Video

Send video by including a video/* attachment in attachments[]. Supported on iMessage, SMS/MMS (carrier-dependent), and WhatsApp.

Preferred video formats by channel (sent as-is):

ChannelPreferred format
iMessageAny common container (MP4, MOV, M4V) — no server-side transcoding
WhatsAppMP4 with H.264 video and AAC audio

WhatsApp specifically: H.264 in other containers (.mov, .mkv, etc.) is repackaged into MP4 without re-encoding. Any other format is transcoded to H.264/AAC in MP4 before send; transcoding adds latency proportional to input size and codec.

Base64 Attachments

Base64 attachment (iMessage):

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": { "text": "Here is the file" },
    "attachments": [
      {
        "type": "base64",
        "data": "iVBORw0KGgoAAAANSUhEUg...",
        "mimeType": "image/png",
        "filename": "chart.png"
      }
    ]
  }'

Send 2 or more image URLs in content.mediaUrls to send an image carousel:

curl -X POST https://api.textbubbles.com/v1/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155551234",
    "content": {
      "text": "Check out these photos!",
      "mediaUrls": [
        "https://cdn.example.com/photo1.jpg",
        "https://cdn.example.com/photo2.png",
        "https://cdn.example.com/photo3.webp"
      ]
    },
    "effect": "fireworks"
  }'

Images must be HTTPS URLs pointing to image files (jpg, png, gif, webp). Minimum 2 images, maximum 20 per message.

Channel Routing

By default, messages try iMessage first and fall back to SMS. Customize with the routing field:

{
  "to": "+14155551234",
  "content": { "text": "Hello!" },
  "routing": {
    "preference": ["imessage", "sms"],
    "fallback": true
  }
}
  • preference — ordered list of channels: "imessage", "sms", "whatsapp"
  • fallback — try the next channel if the first fails (default: true)

Webhook event reliability varies by channel. SMS in particular does not emit message.read, only emits message.delivered on a best-effort basis (depends on the carrier returning a receipt), and SMS “reactions” arrive as plain message.inbound text rather than message.reaction. See Per-Channel Reliability for the full matrix before relying on a specific event for a given channel.

WhatsApp

Include "whatsapp" in the preference list. The WhatsApp send uses the same phone number as your iMessage/SMS line — pairing is per-instance and managed via the admin panel.

{
  "to": "+14155551234",
  "content": { "text": "Hello from WhatsApp!" },
  "routing": { "preference": ["whatsapp"] }
}

You can also fall back from WhatsApp to iMessage/SMS:

{ "routing": { "preference": ["whatsapp", "imessage", "sms"] } }

Send to an existing WhatsApp group by passing the group JID as to:

{
  "to": "120363012345678901@g.us",
  "content": { "text": "Hi team" },
  "routing": { "preference": ["whatsapp"] },
  "mentions": [{ "address": "+14155551234", "start": 0, "length": 3 }]
}

Mentions are honoured on group sends only and silently dropped on individual chats. iMessage-only fields (effect, messageType: "carousel") are dropped on a WhatsApp send. See the WhatsApp reference for capabilities, limitations, and pairing.

Unsend a Message

Retract a sent iMessage:

curl -X POST https://api.textbubbles.com/v1/messages/msg_abc123/unsend \
  -H "Authorization: Bearer YOUR_API_KEY"

Retry a Failed Message

Re-send a message that previously landed in status: failed (for example, because of a transient delivery failure). Only messages currently in status: failed can be retried — messages that are still queued, sent, or in any other state return 400 INVALID_STATUS.

curl -X POST https://api.textbubbles.com/v1/messages/msg_abc123/retry \
  -H "Authorization: Bearer YOUR_API_KEY"

Response (202 Accepted):

{
  "success": true,
  "data": {
    "id": "msg_abc123",
    "status": "queued",
    "retryCount": 1
  }
}

The retry preserves the original recipient, content, attachments, routing preferences, and instance. A fresh provider message ID is issued on the new send — from iMessage’s perspective this is a new send that carries the same TextBubbles message ID. You can call this endpoint multiple times; retryCount increments on each attempt. Watch the usual message.queuedmessage.sent / message.failed webhooks to track the outcome.

Edit a Message

Update sent message content (iMessage only):

curl -X PUT https://api.textbubbles.com/v1/messages/msg_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Updated message text",
    "backwardsCompatibilityMessage": "* Updated message text"
  }'
  • text — the new message text (required)
  • backwardsCompatibilityMessage — fallback text shown on older devices that don’t support message editing (optional)

Idempotency

Include an idempotencyKey to prevent duplicate sends during retries:

{
  "to": "+14155551234",
  "content": { "text": "Your order #12345 has shipped!" },
  "idempotencyKey": "shipment-notification-12345"
}

If a message with the same key already exists, the API returns 200 OK with the existing message.

Full Request Schema

FieldTypeRequiredDescription
tostringConditionallyRecipient phone number (E.164) or iMessage email address. Required unless chatId is provided; mutually exclusive with chatId
chatIdstring (UUID)ConditionallyConversation UUID (the chat.chatId returned by POST /v1/chats/groups). Sends into the existing chat; channel and instance are pinned. Required unless to is provided; mutually exclusive with to
fromstringNoSender address (must be authorized for your customer; uses default if omitted). When chatId is supplied, must resolve to the chat’s pinned instance
content.textstringNoMessage text (up to 10,000 chars). Optional if content.mediaUrls is provided
content.mediaUrlsstring[]NoArray of 1-20 HTTPS image URLs. When 2+ URLs are provided, sends as an image carousel
routing.preferencestring[]NoChannel priority (default: ["imessage", "sms"])
routing.fallbackbooleanNoEnable fallback (default: true)
replyTostringNoMessage ID to reply to (iMessage and WhatsApp)
attachmentsobject[]NoFile attachments. iMessage and SMS/MMS; on SMS each attachment is delivered as a separate MMS, carrier-dependent for non-image MIME types.
attachments[].typestringNoAttachment type: "url" or "base64"
attachments[].urlstringNoURL of the attachment (when type is url)
attachments[].datastringNoBase64-encoded file data (when type is base64)
attachments[].mimeTypestringNoMIME type of the attachment
attachments[].filenamestringNoOriginal filename
attachments[].isAudioMessagebooleanNoRender as a native voice-memo bubble (inline player). Supported on iMessage and WhatsApp; the server transcodes non-compatible inputs to the channel’s preferred format (Opus-in-CAF for iMessage, Opus-in-OGG for WhatsApp). Other channels deliver the file as a generic attachment.
effectstringNoMessage effect (iMessage only)
scheduledAtstringNoISO 8601 datetime for scheduled delivery
idempotencyKeystringNoUnique key for deduplication
callbackUrlstringNoPer-message webhook URL
metadataobjectNoCustom key-value data