Messagesv4

Send a message

POST/messages

The primary endpoint for starting a message. The common send is entirely scalar: { "from": "+15551230001", "to": "+15551234567", "text": "Hello" }.

from is one optional string:

  1. Phone number"from": "+15551230001" selects your exact numbered channel with that address.
  2. Alias"from": "support" selects the non-numbered channel (RCS Business) with that organization-wide alias.
  3. Channel id"from": "ch_..." is the advanced exact-channel form.
  4. Automatic — omit from to use the key's default priority / automatic routing.

Advanced routing controls (each mutually exclusive with from): "priority_id": "priority_..." routes through an explicit priority; "channel_type": "blooio" constrains automatic/priority routing to one type; "hybrid": true (or a representative phone number string) requests hybrid Twilio-first / Blooio-after-reply routing.

Priority routing is history-first and content-aware. Poll content currently routes only through Blooio. Existing groups require an exact sender whose channel owns the group or a hybrid sender that owns that channel. Returns the resolved scalar from plus the chosen channel_id, chat_id, and routing explanation.

Headers

AuthorizationRequiredstring

Your API key, sent as a bearer token: Authorization: Bearer <api_key>. Editing this stays in sync with the API key box on the right.

Bearer
Idempotency-Keyoptionalstring

Optional key for safe retries. Replaying the same key with an identical body returns the original result; a different body returns 409. Use a unique value (such as a UUID) per logical request.

Body parameters

JSON
textoptionalstring

Plain text message. May be combined with attachments (sent as media with a caption) — except a lone audio file, which is sent as a standalone voice memo and cannot carry a caption. Set format: "markdown" to interpret Markdown styling in this field (and in parts[].text).

formatoptionalstring

How to interpret text (and each parts[].text). Defaults to plain, which sends the string exactly as given. With markdown, four constructs are parsed and delivered as real iMessage rich text — the recipient sees styled text, not delimiters: | Construct | Syntax | | --- | --- | | Bold | **bold** or __bold__ | | Italic | *italic* or _italic_ | | Underline | ++underline++ | | Strikethrough | ~~strike~~ | They nest freely (**bold and _italic_**). Everything else Markdown can express — headings, lists, links, code spans, blockquotes, images — is NOT styling iMessage can carry, so it is passed through as literal characters: [Blooio](https://blooio.com) is delivered with its brackets and URL intact, and # Heading keeps its #. Escape a delimiter with a backslash (\*not italic\*) to send it literally. The styling travels in the message's attributed body, so the stored text and the text returned on reads and webhooks is always the plain string the recipient sees, with the delimiters removed. The Markdown itself comes back as formatted_text, re-serialized into a normalized spelling rather than echoed verbatim (__bold__ returns as **bold**). Only valid on Blooio iMessage channels — 422 format_unsupported_for_channel_type on any other channel type, since no other channel type has a rich-text equivalent and would otherwise deliver your delimiters as literal text. Rich text also requires the message to be delivered over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain text (the text string), because SMS cannot carry styling. Rejected with 422 invalid_content when set without text or parts, when the Markdown source exceeds 20000 characters, or when it compiles to more than 256 distinct formatting ranges.

"plain""markdown"
attachmentsoptionalstring[]

Public https media URLs. May be combined with text. On Blooio iMessage, two or more image/video URLs are grouped into one photo/video carousel/collage bubble by default; set carousel: false to send them as separate bubbles. **Voice memos:** a single audio file (.mp3, .m4a, .wav, .aac, .opus, .ogg) is automatically sent as a voice memo (the native waveform/scrubber bubble), not a plain audio-file attachment — no extra field is needed. A voice memo is a standalone bubble, so it cannot be combined with text or any other attachment; do that and the send is rejected with 422 invalid_content. To send audio with a message, send the voice memo and the text as two separate requests.

app_clipoptionalobject

An App Clip bubble (Blooio p2p iMessage only). Sends an App Clip launch URL that Messages renders as a native App Clip card (app icon + name + Open button); tapping it launches the instant App Clip experience without an install. Provide either a ready-made url (Apple's https://appclip.apple.com/id?p=<bundleID> launcher, or an app's own associated-domain App Clip link) or the bundle_id convenience field, which is expanded into the canonical Apple launch URL. Tip: App-Clip availability is region/OS gated by Apple; an unavailable clip renders a card that falls back to the App Store. To deep-link into an already-installed app instead (e.g. open a specific screen in a downloaded app), send that app's universal link via rich_link.

urloptionalstring

The App Clip launch URL (https). Mutually exclusive with bundle_id; if both are given, url wins.

bundle_idoptionalstring

App Clip bundle identifier (e.g. com.example.app.Clip), expanded to https://appclip.apple.com/id?p=<bundle_id>. Used when url is absent.

titleoptionalstring

Optional preview title override.

imessage_appoptionalobject

Bring-your-own iMessage App Extension bubble (Blooio p2p iMessage only). Blooio is the transport, NOT the renderer: you point at YOUR OWN signed iMessage app extension and we assemble the MSMessage app balloon and send it. Recipients who have your app installed render its interactive overlay (its MSMessagesAppViewController reads url on tap); recipients who don't see the native template card (from caption/image_url) plus Apple's built-in App Store fallback. Identify your extension with bundle_id + team_id (used to form the com.apple.messages.MSMessageExtensionBalloonPlugin:<team_id>:<bundle_id> plugin id) and pass the app-state url your extension understands. This does NOT ship or host a renderer — the recipient must have your app for the interactive experience. (For instant, no-install experiences use app_clip; to deep-link into an already-installed app use rich_link.)

bundle_idRequiredstring

Your iMessage app extension's bundle identifier (e.g. com.acme.app.MessagesExtension).

team_idRequiredstring

Your Apple Developer Team ID. Combined with bundle_id to form the balloon plugin id.

urlRequiredstring

The app-state URL your extension reads on tap (its MSMessage.url). Carries the state to reconstruct on the recipient device.

app_nameoptionalstring

Display name shown for the app (the an field). Defaults to a generic label.

captionoptionalstring

Template-card caption shown to everyone (including recipients without the app).

subcaptionoptionalstring

Optional template-card subcaption.

image_urloptionalstring

Optional https image used as the fallback template card thumbnail. Fetched best-effort (≤256KB); a fetch failure never fails the send.

app_store_idoptionalinteger

Optional App Store id (adam id) for your app. When the recipient doesn't have the extension installed, Messages uses it to deep-link the App Store "get this app" fallback to the correct listing. Omitting it leaves the send unchanged.

polloptionalobject

A native Blooio poll. Routed sends automatically consider only poll-capable channels.

titleRequiredstring
optionsRequiredstring[]
interactiveoptionalobject

Interactive content (WhatsApp Business / RCS). Requires kind.

kindRequiredstring
templateoptionalobject

Template content (WhatsApp Business).

template_idoptionalstring
partsoptionalobject[]

Ordered text/media parts sent as one multipart message (Blooio only). When every part is an image/video (two or more, no text parts) Messages renders them as a photo/video carousel/collage by default; set carousel: false to opt out. With format: "markdown", each part's text is parsed for styling independently.

Array of object

textoptionalstring
urloptionalstring
reply_tooptionalstring

Message id to reply to. Applies to the whole outgoing message.

effectoptionalstring

iMessage screen/bubble effect (Blooio, text only).

badgeoptionalstring

Optional attribution badge shown under the bubble: sent_with_siri renders "Sent with Siri" and sent_with_facetime renders "Sent with FaceTime", the way a message dictated to Siri or sent during a FaceTime call appears. Omit for no badge. Any other value is rejected with 422 invalid_content. Applies to text, attachments (media), parts (multipart), rich_link, and app_clip sends on Blooio iMessage channels only; rejected with 422 invalid_content on other content types. No effect on non-Blooio channels.

"sent_with_siri""sent_with_facetime"
fromoptionalFrom

The sender as a single string: a phone number (any common format; matched to your exact numbered channel), a channel alias (organization-wide name of a non-numbered channel), or an exact channel id (ch_...). Omit it for automatic routing. If a phone number ever matches more than one active channel type, the send fails with 409 ambiguous_from and the exact ch_... id must be used instead.

priority_idoptionalstring

Route through an explicit priority (priority_...). Mutually exclusive with from.

channel_typeoptionalstring
"blooio""twilio""whatsapp""whatsapp_business""rcs_business"
hybridoptionalboolean | string

Hybrid (Twilio-first / Blooio-after-reply) routing. true uses the calling key's hybrid configuration; a phone number string selects the hybrid owner of that representative number. Mutually exclusive with from and priority_id.

toRequiredRecipient

A recipient: a raw identifier string (phone/email/opaque provider id), an array of identifier strings (multiple recipients), an existing { group_id } with an exact or hybrid owner sender, or (for routed sends) a { contact_id }. A contact_id enables cross-channel-type routing via the contact's identities; a bare identifier reaches phone/email-addressable channels only.

dry_runoptionalboolean

Validate and route only; do not send.

Returns

idoptionalstring
chat_idoptionalstring
channel_idoptionalstring
channel_typeoptionalstring
"blooio""twilio""whatsapp""whatsapp_business""rcs_business"
protocoloptionalstring

Transport state or resolved wire service; never null. pending = accepted and dispatched, wire service not resolved yet; imessage / rcs / sms = the device-confirmed transport (iMessage blue bubble, RCS, or SMS/MMS green bubble); whatsapp = sent over P2P WhatsApp; whatsapp_business = sent over the WhatsApp Business Platform (WABA); unknown = accepted but the transport could not be resolved before the tracking window closed (see error).

"pending""unknown""imessage""sms""rcs""whatsapp""whatsapp_business"
fromoptionalstring | null

The resolved sender as a human value: the sending channel's phone number (numbered channels) or alias (non-numbered channels). This is the primary sender reference; channel_id remains available for exact follow-up and audit.

directionoptionalstring
"inbound""outbound"
typeoptionalstring
statusoptionalstring

Initial lifecycle state for the send: normally queued (accepted for delivery), or failed if rejected before dispatch. Later transitions (sentdelivered / read, or failed) are reported via the message status endpoint and message.* webhooks.

group_idoptionalstring

Existing group id for a group-targeted send.

hybridoptionalobject

Hybrid routing phase and optional transition placement.

erroroptionalobject | null
fallbackoptionalobject

Present when a send could not be delivered on the requested channel and a different channel should be tried.

reasonoptionalstring
tooptionalstring

Recipient identifier. Present only on per-recipient items inside a fan-out (FanOutResult) response.

dry_runoptionalboolean
would_sendoptionalboolean
previewoptionalobject
polloptionalobject
titleoptionalstring
optionsoptionalstring[]
routingoptionalRoutingMetadata

How Blooio selected the concrete sender channel.

modeoptionalstring
"explicit""priority""hybrid"
channel_typeoptionalstring
"blooio""twilio""whatsapp""whatsapp_business""rcs_business"
numberoptionalstring
aliasoptionalstring
priority_idoptionalstring
priorityoptionalinteger

Response codes

200Dry-run preview
201Message accepted. The message object is returned with its current `status`; a synchronously rejected send returns `status: "failed"` with an `error` object (still 201, since the message was created).
207Multi-recipient fan-out with mixed results (some recipients sent, some failed). See per-recipient items in `data`.
401Your API key is missing or invalid. Pass it as a bearer token.
403Blocked by messaging safety. The error `code` identifies the gate: `safety_account_review` (the sending number is blocked pending review), `safety_reply_only` (outbound is restricted to conversations with prior inbound), `safety_new_conversations_paused` (brand-new conversations are temporarily paused; replies are unaffected), or `conversation_content_restricted` (links, media, and attachments are not allowed before the recipient's first written reply, or in a re-engagement of an inactive conversation — an emoji reaction does not unlock them). Safety actions clear automatically as the underlying pattern ages out; current per-number state is delivered via the `safety.state_changed` webhook event.
404No message was found.
409The request conflicts with the current state of the message (e.g. it already exists, the chat is closed, or the channel isn't active).
422Validation failed, or a multi-recipient fan-out in which every recipient failed (in which case the body is a `FanOutResult` rather than an error envelope).
429Conversation-state limit reached. The error `code` identifies the rule: `conversation_awaiting_reply` (up to 3 messages may be sent before the recipient responds at all), `conversation_streak_limit` (consecutive-message cap since their last response), or `conversation_inactive_paused` (no response in 14+ days and the single re-engagement message was already sent). Every response body carries `conversation_state` — one of `new`, `acknowledged` (reacted but never wrote back), `active`, `established`, `trusted`, or `inactive` — and the streak errors also carry `current_streak` and `limit`, so you can see which rung of the ladder a thread is on. The cap rises with the thread's written history, and an **emoji reaction counts as a response**: a tapback clears the consecutive-message counter and keeps a thread from going inactive, so the limit lifts the moment the recipient reacts. A reaction is not treated as a reply, though — only writing back raises the cap and unlocks links, media, and attachments (see `conversation_content_restricted`).
501This message endpoint is defined but not implemented yet.

Sends a live request with your values and shows the real response below. Your key is stored only in this browser.

Request
curl -X POST https://api.blooio.com/v4/messages \
Body object
Response objectexample
{  "id": "obj_a1b2c3d4",  "chat_id": "chat_a1b2c3d4",  "channel_id": "ch_a1b2c3d4",  "channel_type": "blooio",  "protocol": "pending",  "from": "+15551234567",  "direction": "inbound",  "type": "string",  "status": "active",  "group_id": "grp_a1b2c3d4",  "hybrid": {},  "error": {},  "fallback": {    "recommended": false,    "reason": "string"  },  "to": "+15551234567",  "dry_run": false,  "would_send": false,  "preview": {},  "poll": {    "title": "Example title",    "options": [      "string"    ]  },  "routing": {    "mode": "explicit",    "channel_type": "blooio",    "number": "+15551234567",    "alias": "string",    "priority_id": "obj_a1b2c3d4",    "priority": 0  }}