Channelsv4

Send a message on a specific channel

POST/channels/{channel}/messages

Channel-scoped equivalent of POST /messages with an explicit from: :channel accepts the same phone number (URL-encode + as %2B), alias, or ch_... value from accepts, and the body omits from. Multiple recipients on a group-capable channel (blooio/whatsapp) form a single group thread; on other channels they fan out to individual sends (max 100).

Path parameters

channelRequiredstring

The channel reference — the same three forms from accepts: a phone number (URL-encode + as %2B), a channel alias, or the exact channel id (ch_...).

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. On Apple Messages for Business (AMB) this renders as a native interactive message: a quick reply, list picker, time picker, dynamic form, Apple Pay request, authentication (Sign in) request, or a custom iMessage app. Select the variant with kind and populate that kind's required fields; fields not relevant to the chosen kind are ignored. Each kind is validated against the minimum shape Apple needs to render it BEFORE the message is sent: Apple's gateway accepts malformed payloads and then shows an empty bubble on device, so Blooio rejects them up front with a 422 and a kind-specific code (invalid_quick_reply, invalid_list_picker, invalid_time_picker, invalid_form, invalid_apple_pay, invalid_authenticate, invalid_imessage_app) whose message names the exact field/index that is wrong. Free-form objects (dynamic, payment, authenticate) are otherwise forwarded to Apple verbatim. Before sending, the chosen kind is also checked against the device capability-list Apple stamps on the conversation's latest inbound message: when a list is known and does not advertise the required capability, the send is rejected with 422 amb_capability_unavailable (naming the missing Apple capability) rather than delivering a bubble the device can't render.

kindRequiredstring

Which interactive experience to render.

"quick_reply""list_picker""time_picker""form""apple_pay""authenticate""imessage_app"
summary_textoptionalstring

Prompt / fallback line shown where the interactive UI can't render. Used as the quick reply prompt.

textoptionalstring

Alias for summary_text.

request_identifieroptionalstring

Optional stable id echoed back on the customer's reply. Defaults to the message id.

received_messageoptionalobject

Bubble shown to the customer before they interact. Not allowed for quick_reply (send a plain text bubble first instead).

titleoptionalstring
subtitleoptionalstring
styleoptionalstring
"icon""small""large"
imageIdentifieroptionalstring
reply_messageoptionalobject

Bubble shown after the customer replies.

titleoptionalstring
subtitleoptionalstring
styleoptionalstring
"icon""small""large"
imageIdentifieroptionalstring
imagesoptionalobject[]

Inline PNG assets referenced by imageIdentifier elsewhere in the payload. Supply each as base64 data OR a url (fetched server-side). Every image — fetched or inline — is validated from its MAGIC BYTES (not the Content-Type): it must be a real PNG (422 amb_image_not_png / code on the failed send otherwise), within Apple's 200 KB per-image cap (amb_image_too_large) and 5 MB aggregate cap (amb_images_total_too_large), and within the @3x dimension ceiling — supply expected_size: { width, height, style } to enforce an exact @3x size, rejected with amb_image_bad_dimensions naming the expected size. When the resulting interactiveData would exceed Apple's ~10 KB inline ceiling, Blooio automatically uploads it and sends it by interactiveDataRef instead (Apple's 200 on an over-limit inline payload is not treated as conformance).

Array of object

identifieroptionalstring
dataoptionalstring

Base64-encoded PNG.

urloptionalstring

Public https PNG URL, fetched and validated server-side.

expected_sizeoptionalobject

Optional exact @3x size to enforce for this asset's target style.

widthoptionalinteger
heightoptionalinteger
styleoptionalstring
itemsoptionalobject[]

Quick reply options (kind = quick_reply). Required: 1–5 items, each with a non-empty id and title. Rejected with 422 invalid_quick_reply otherwise.

Array of object

idRequiredstring
titleRequiredstring
sectionsoptionalobject[]

List picker sections (kind = list_picker). Required: at least one section, each with a non-empty title and at least one item; every item needs a non-empty identifier and title. Rejected with 422 invalid_list_picker otherwise. Blooio normalizes the input into Apple's reliably-rendering wire shape (listPicker.sections[].items[] with a per-item style, under a live-layout envelope) — the shape confirmed to both render the bubble and populate options on tap. May also be supplied as list (an array of sections or an object with a sections array).

Array of object

titleRequiredstring
orderoptionalinteger
multipleSelectionoptionalboolean

Allow selecting more than one item in this section.

itemsRequiredobject[]

Array of object

identifierRequiredstring
titleRequiredstring
subtitleoptionalstring
orderoptionalinteger
styleoptionalstring

Row style. Defaults to default when omitted.

"default"
imageIdentifieroptionalstring
listoptionalobject

Alias for sections (kind = list_picker). Accepts { "sections": [...] } (or a bare array of sections). Same validation and normalization as sections.

eventoptionalobject

Time picker event (kind = time_picker). Required: at least one timeslots entry, each with a startTime (ISO-8601) and a positive duration (seconds). Rejected with 422 invalid_time_picker otherwise.

identifieroptionalstring
titleoptionalstring
locationoptionalobject
titleoptionalstring
timezoneOffsetoptionalinteger
timeslotsoptionalobject[]

Array of object

identifieroptionalstring
startTimeRequiredstring

ISO-8601 start time; normalized to Apple's +0000 format on send.

durationRequiredinteger

Slot duration in seconds.

dynamicoptionalobject

Apple dynamic interactive message (multi-page form) object, forwarded verbatim (kind = form). Required: at least one page under pages (or data.pages); also carries startPageIdentifier, requestIdentifier, showSummary, etc. Rejected with 422 invalid_form otherwise. May also be supplied under form.

formoptionalobject

Alias for dynamic (kind = form).

paymentoptionalobject

Apple Pay payment request object, forwarded verbatim (kind = apple_pay). Required: a paymentRequest object with total (label + numeric amount), currencyCode (ISO-4217), and countryCode (ISO-3166). Rejected with 422 invalid_apple_pay otherwise. Apple additionally gates delivery on the merchant configuration.

authenticateoptionalobject

OAuth2 authentication request object, forwarded verbatim (kind = authenticate). Required: an oauth2 object with responseType, scope, and redirectURI. Rejected with 422 invalid_authenticate otherwise. Sent as interactive data version 2.0.

oauth2optionalobject

Shorthand for authenticate.oauth2 (kind = authenticate). Requires responseType, scope, and redirectURI.

appoptionalobject

Custom iMessage app metadata (kind = imessage_app). These App Store fallback fields (shown by Messages when the extension isn't installed) may also be supplied flat on the interactive object as app_id / app_name / app_icon; both forms reach the wire.

app_idoptionalinteger
app_nameoptionalstring
app_iconoptionalstring
bidoptionalstring
urloptionalstring
app_idoptionalinteger | string

App Store id for the iMessage app fallback (kind = imessage_app). Flat alias for app.app_id.

app_nameoptionalstring

App Store name for the iMessage app fallback (kind = imessage_app). Flat alias for app.app_name.

app_iconoptionalstring

Base64 PNG icon for the iMessage app fallback (kind = imessage_app). Flat alias for app.app_icon.

bidoptionalstring

iMessage extension bundle id. Required for kind = imessage_app (or supply it under app.bid); rejected with 422 invalid_imessage_app otherwise.

urloptionalstring

Custom app URL / query string (kind = imessage_app).

session_identifieroptionalstring
use_live_layoutoptionalboolean
invitationoptionalobject

Apple Messages for Business (AMB) invitation. Sends an Apple-managed notification template (Live Layout) that invites the customer into a business chat. Addressed to a phone number (tel:+E164) rather than an opaque AMB id. Requires prior Invitations approval from Apple and explicit customer opt-in.

template_idRequiredstring

The Apple-managed notification template id authorized for your business (e.g. binaryChoice.engage.withImage).

reference_idRequiredstring

Your business context id for this invitation (order number, case id, etc.). Required by Apple and echoed back on the CloseSession webhook. Max 1000 characters; must not contain quotation marks or apostrophes.

parametersoptionalobject

Template parameter values required by the chosen template (e.g. brandName, and brandLogo as a base64 PNG). For known templates the required parameters are enforced before sending — e.g. binaryChoice.engage.withImage requires brandName + brandLogo — and a missing one is rejected with 422 invalid_invitation naming the field (rather than being accepted and then rejected by Apple with 400 Invalid payload).

localeoptionalstring

BCP-47 locale for the template. Defaults to en-us.

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"
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""amb"
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); amb = sent over Apple Messages for Business; unknown = accepted but the transport could not be resolved before the tracking window closed (see error).

"pending""unknown""imessage""sms""rcs""whatsapp""whatsapp_business""amb"
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 (sent → delivered / 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.

erroroptionalMessageError

Why the message failed; null unless it failed. On a blooio channel code is a MessageErrorCode and message describes it. On any other channel both are the provider's own error. See [Errors](/errors#v4-message-errors).

codeRequiredMessageErrorCode | string | null
messageRequiredstring | null

A description of the failure.

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""amb"
numberoptionalstring
aliasoptionalstring
priority_idoptionalstring
priorityoptionalinteger

Response codes

200Dry-run preview
201Message accepted.
207Multi-recipient fan-out with mixed results.
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 channel was found with the provided `channel`.
409The request conflicts with the current state of the channel (e.g. it already exists, the chat is closed, or the channel isn't active).
422Validation failed — one or more fields in the request are invalid or missing.
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 channel 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/channels/string/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": {    "code": null,    "message": "Hello from Blooio!"  },  "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  }}