Interactive messages (AMB)
Send Apple Messages for Business quick replies, list pickers, time pickers, and dynamic forms — and read the customer's reply.
Apple Messages for Business (AMB) 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 AMB sender and address the customer by their opaque AMB 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 AMB chat for you.POST /v4/channels/{channel}/messages— channel-scoped send. The sender lives in the URL ({channel}is the AMB 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 AMB it is the channel's alias — a short, stable name (like support) you assign to your connected AMB business 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 AMB 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 AMB identifier — the id Apple issues for that customer, handed to you on the inbound message.received webhook that opened the conversation. AMB 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 AMB sender alias. This is the one field that says who the message is from;supportresolves to your connected AMB business channel."to": "opaque-amb-customer-id"— the customer's opaque AMB 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": "AMBInteractive",
"description": "A Blooio AMB 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.