Interactive messages
Send Apple Messages for Business quick replies, list pickers, time pickers, and dynamic forms — and read the customer's reply.
Apple Messages for Business supports rich, tappable messages that render natively in the Messages app. Send them through the standard POST /messages endpoint with a top-level interactive field and a kind:
kind |
What the customer sees |
|---|---|
quick_reply |
A prompt with up to a few tappable options |
list_picker |
One or more sections of selectable rows (single or multi-select) |
time_picker |
A set of appointment time slots to choose from |
form |
A multi-page dynamic form (text inputs, pickers, dates) |
apple_pay |
An Apple Pay payment request |
authenticate |
An OAuth2 "Sign in" request |
imessage_app |
A custom iMessage extension bubble |
Addressing
Interactive messages are 1:1 only (no groups). Send from an Apple Messages for Business sender and address the customer by their opaque Apple Messages for Business identifier:
{
"from": "support",
"to": "opaque-amb-customer-id",
"interactive": { "kind": "quick_reply", "...": "..." }
}Two endpoints accept this exact body — pick whichever matches how you track senders. Both share one implementation and one field set:
POST /v4/messages— one-shot send. Passfromin the body; the API finds or creates the Apple Messages for Business chat for you.POST /v4/channels/{channel}/messages— channel-scoped send. The sender lives in the URL ({channel}is the Apple Messages for Business alias, e.g./v4/channels/support/messages), so the body omitsfrom.
If you already hold the chat, reply with POST /v4/chats/{chatId}/messages — the chat already knows its channel, so you omit from there too.
Request fields
Every interactive send is built from three top-level fields. Open the POST /v4/messages reference to see them (and their JSON schema) in the interactive API explorer.
| Field | Type | What it is |
|---|---|---|
from |
string | Which of your senders the message goes out from. It is one string, and for Apple Messages for Business it is the channel's alias — a short, stable name (like support) you assign to your connected Apple Messages for Business account in the dashboard, not a phone number. (A phone number or a ch_… channel id are also valid from values for other channel types.) Blooio resolves the alias to the underlying Apple Messages for Business channel at send time, so you can transfer the alias to a replacement integration without changing your code. Omit from on the channel-scoped endpoint (it's in the path) and on the chat endpoint (the chat fixes it). |
to |
string | Who receives the message. The customer's opaque Apple Messages for Business identifier — the id Apple issues for that customer, handed to you on the inbound message.received webhook that opened the conversation. Apple Messages for Business is always customer-initiated, so you have this value before you ever reply. It is not a phone number or email. |
interactive |
object | The interactive content itself. Its kind selects the experience (quick_reply, list_picker, …) and the rest of the object depends on that kind. See Common fields and each kind's section below; the full machine-readable schema is the interactive field on POST /v4/messages. |
Common fields
These apply to every kind:
summary_text(ortext) — a plain-text fallback shown where interactive UI can't render. It is also the quick reply prompt.received_message— the bubble the customer sees before interacting (title,subtitle,style, optionalimageIdentifier).reply_message— the bubble shown after they reply.request_identifier— an optional stable id echoed back on the reply (defaults to the message id).images— inline base64 PNGs referenced byimageIdentifier.
Note
quick_replymust not includereceived_message/reply_message— Apple rejects the send. Send a plain text bubble first to introduce the prompt, then the quick reply.
Quick reply
curl -X POST https://api.blooio.com/v4/messages \
-H "Authorization: Bearer bl_live_..." \
-H "Content-Type: application/json" \
-d '{
"from": "support",
"to": "opaque-amb-customer-id",
"interactive": {
"kind": "quick_reply",
"summary_text": "How can we help?",
"items": [
{ "id": "sales", "title": "Talk to sales" },
{ "id": "support", "title": "Get support" },
{ "id": "hours", "title": "Store hours" }
]
}
}'Try itReading that request line by line — every field maps to the POST /v4/messages reference:
POST https://api.blooio.com/v4/messages— the one-shot send endpoint. To send from the sender in the URL instead, callPOST /v4/channels/support/messagesand dropfromfrom the body.Authorization: Bearer bl_live_...— your Blooio API key; every call is authenticated as a bearer token, and the key's organization scopes which sendersfromcan resolve."from": "support"— the Apple Messages for Business sender alias. This is the one field that says who the message is from;supportresolves to your connected Apple Messages for Business channel."to": "opaque-amb-customer-id"— the customer's opaque Apple Messages for Business id from the inbound webhook. This is who receives it."interactive"— the flat content field. Its presence makes this an interactive message (instead oftext,attachments, etc.)."kind": "quick_reply"— render a quick reply. Every other field insideinteractiveis interpreted relative to thiskind."summary_text"— the prompt shown above the buttons and the plain-text fallback where the UI can't render."items"— the tappable options. Eachidis echoed back on the customer's reply so you know what they picked; eachtitleis the button label.
A successful call returns a queued message (id, chat_id, channel_id, status). Track delivery with GET /v4/chats/{chatId}/messages/{messageId}/events or a webhook.
List picker
Sections of selectable rows. Set multipleSelection to let the customer choose more than one row in a section.
{
"kind": "list_picker",
"received_message": { "title": "Choose a service", "style": "small" },
"reply_message": { "title": "Thanks — booking you in", "style": "small" },
"sections": [
{
"title": "Services",
"multipleSelection": false,
"items": [
{ "identifier": "haircut", "title": "Haircut", "subtitle": "30 min" },
{ "identifier": "color", "title": "Color", "subtitle": "90 min" }
]
}
]
}Time picker
Offer appointment slots. startTime accepts ISO-8601 (it is normalized to Apple's format on send); duration is in seconds.
{
"kind": "time_picker",
"received_message": { "title": "Pick a time for your consultation" },
"event": {
"title": "Consultation",
"location": { "title": "Main St Clinic" },
"timeslots": [
{ "startTime": "2026-07-20T17:00:00Z", "duration": 3600 },
{ "startTime": "2026-07-20T18:00:00Z", "duration": 3600 }
]
}
}Dynamic form
A form is a multi-page flow (text inputs, list/date pickers, a summary). The dynamic object is forwarded to Apple's Dynamic Interactive Message payload verbatim, so it uses Apple's field names (pages, startPageIdentifier, requestIdentifier, per-page textInputs / datePicker / listPicker, etc.).
{
"kind": "form",
"received_message": { "title": "Book an appointment" },
"reply_message": { "title": "Got it — we'll confirm shortly" },
"dynamic": {
"version": "1.0",
"requestIdentifier": "booking-42",
"startPageIdentifier": "details",
"showSummary": true,
"pages": [
{
"pageIdentifier": "details",
"title": "Your details",
"nextPageIdentifier": "when",
"textInputs": [
{ "identifier": "name", "title": "Full name", "type": "name" },
{ "identifier": "email", "title": "Email", "type": "email" }
]
},
{
"pageIdentifier": "when",
"title": "Preferred date",
"datePicker": { "identifier": "date", "title": "Date" }
}
]
}
}You can also supply the form object under form instead of dynamic.
Apple Pay & authentication
kind: "apple_pay"— put Apple's payment request underpayment. Payment callbacks are relayed to the channel's payment webhook.kind: "authenticate"— put the OAuth2 request underauthenticate(or a bareoauth2object). Sent as interactive data version 2.0 via Apple's authenticate endpoint.
Custom iMessage app
kind: "imessage_app" renders a custom Messages extension. It requires the extension's own bundle id in bid (com.apple.messages.MSMessageExtensionBalloonPlugin:{team-id}:{ext-bundle-id}):
{
"kind": "imessage_app",
"bid": "com.apple.messages.MSMessageExtensionBalloonPlugin:TEAMID:com.brand.ext",
"app": { "app_id": 123, "app_name": "Brand App" },
"url": "?data=abc",
"use_live_layout": true,
"received_message": { "title": "Open the app", "subtitle": "Tap to continue" }
}Reading the reply
Interactive replies arrive as a normal inbound message webhook. Because the tapped choice has no plain body, Blooio fills the message text with a human-readable summary of what the customer chose (the option title, or form answers joined together) and attaches a structured metadata.interactive object:
{
"role": "reply",
"kind": "list_picker",
"label": "List Picker",
"chosen": ["Haircut"]
}For forms, metadata.interactive also includes a per-page responses array so you can map answers back to your fields:
{
"role": "reply",
"kind": "form",
"chosen": ["J. Appleseed", "jappleseed@example.com", "03/30/2026"],
"responses": [
{
"page_identifier": "details",
"question": "Your details",
"answers": [
{ "identifier": "name", "type": "name", "title": "J. Appleseed", "value": "J. Appleseed" },
{ "identifier": "email", "type": "email", "title": "jappleseed@example.com", "value": "jappleseed@example.com" }
]
}
]
}Note Large replies (common for multi-page forms) arrive from Apple as a reference that Blooio resolves after acknowledging the inbound. The message
textandmetadata.interactiveare backfilled once resolved, so the webhook fires with the complete reply.
Capabilities
A device advertises which interactive features it supports. Blooio surfaces this on the chat and via channel capabilities; Apple's send endpoint is the authoritative gate and returns a clear error if a feature is genuinely unsupported.
Structured output for AI agents
If you generate interactive messages with an LLM, constrain the model to the JSON Schema below (e.g. OpenAI response_format: { type: "json_schema" }, or a tool/function parameter schema) so it always emits a valid interactive object. Put the generated object under the top-level interactive field of POST /messages alongside scalar from and to:
{ "from": "support", "to": "opaque-amb-customer-id", "interactive": { "kind": "quick_reply", "...": "..." } }{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AppleMessagesInteractive",
"description": "A Blooio Apple Messages for Business interactive object (the value of the top-level `interactive` send field). Choose exactly one kind and fill only that kind's fields.",
"type": "object",
"oneOf": [
{
"title": "quick_reply",
"type": "object",
"required": ["kind", "summary_text", "items"],
"additionalProperties": false,
"properties": {
"kind": { "const": "quick_reply" },
"summary_text": { "type": "string", "description": "Prompt shown above the options." },
"items": {
"type": "array",
"minItems": 1,
"maxItems": 10,
"items": {
"type": "object",
"required": ["id", "title"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "description": "Stable option id returned on the reply." },
"title": { "type": "string", "description": "Short tappable label." }
}
}
}
}
},
{
"title": "list_picker",
"type": "object",
"required": ["kind", "sections"],
"additionalProperties": false,
"properties": {
"kind": { "const": "list_picker" },
"received_message": { "$ref": "#/$defs/bubble" },
"reply_message": { "$ref": "#/$defs/bubble" },
"sections": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["title", "items"],
"additionalProperties": false,
"properties": {
"title": { "type": "string" },
"multipleSelection": { "type": "boolean" },
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["identifier", "title"],
"additionalProperties": false,
"properties": {
"identifier": { "type": "string" },
"title": { "type": "string" },
"subtitle": { "type": "string" }
}
}
}
}
}
}
}
},
{
"title": "time_picker",
"type": "object",
"required": ["kind", "event"],
"additionalProperties": false,
"properties": {
"kind": { "const": "time_picker" },
"received_message": { "$ref": "#/$defs/bubble" },
"reply_message": { "$ref": "#/$defs/bubble" },
"event": {
"type": "object",
"required": ["title", "timeslots"],
"additionalProperties": false,
"properties": {
"title": { "type": "string" },
"location": {
"type": "object",
"additionalProperties": false,
"properties": { "title": { "type": "string" } }
},
"timeslots": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["startTime", "duration"],
"additionalProperties": false,
"properties": {
"startTime": { "type": "string", "description": "ISO-8601, e.g. 2026-07-20T17:00:00Z." },
"duration": { "type": "integer", "description": "Slot length in seconds." }
}
}
}
}
}
}
},
{
"title": "form",
"type": "object",
"required": ["kind", "dynamic"],
"additionalProperties": false,
"properties": {
"kind": { "const": "form" },
"received_message": { "$ref": "#/$defs/bubble" },
"reply_message": { "$ref": "#/$defs/bubble" },
"dynamic": {
"type": "object",
"description": "Apple dynamic interactive message, forwarded verbatim. Uses Apple's field names.",
"required": ["startPageIdentifier", "pages"],
"additionalProperties": true,
"properties": {
"startPageIdentifier": { "type": "string" },
"showSummary": { "type": "boolean" },
"pages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["pageIdentifier", "title"],
"additionalProperties": true,
"properties": {
"pageIdentifier": { "type": "string" },
"title": { "type": "string" },
"subtitle": { "type": "string" },
"nextPageIdentifier": { "type": "string" },
"textInputs": {
"type": "array",
"items": {
"type": "object",
"required": ["identifier", "title"],
"additionalProperties": true,
"properties": {
"identifier": { "type": "string" },
"title": { "type": "string" },
"type": { "type": "string", "description": "Apple input type, e.g. text, name, email, phone." }
}
}
},
"datePicker": {
"type": "object",
"required": ["identifier", "title"],
"additionalProperties": true,
"properties": {
"identifier": { "type": "string" },
"title": { "type": "string" }
}
}
}
}
}
}
}
}
}
],
"$defs": {
"bubble": {
"type": "object",
"additionalProperties": false,
"properties": {
"title": { "type": "string" },
"subtitle": { "type": "string" },
"style": { "type": "string", "enum": ["icon", "small", "large"] }
}
}
}
}Guidance to put in your system prompt:
- Choose exactly one
kindand populate only that kind's fields. quick_replymust not includereceived_message/reply_message— send a plain text bubble first.time_pickerstartTimeis ISO-8601 anddurationis in seconds.- Keep option, row, and section titles short — they render as tappable UI.
Note OpenAI's strict structured outputs don't allow a top-level
oneOf. If you use strict mode, pass a single branch (onekind) from the schema above as yourjson_schema, or use one tool per kind. The same schema is served machine-readable in the reference as theinteractivefield of the send request inopenapi.v4.json.