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. Pass from in 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 omits from.

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 (or text) — 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, optional imageIdentifier).
  • 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 by imageIdentifier.

Note quick_reply must not include received_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 it

Reading 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, call POST /v4/channels/support/messages and drop from from 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 senders from can resolve.
  • "from": "support" — the AMB sender alias. This is the one field that says who the message is from; support resolves 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 of text, attachments, etc.).
    • "kind": "quick_reply" — render a quick reply. Every other field inside interactive is interpreted relative to this kind.
    • "summary_text" — the prompt shown above the buttons and the plain-text fallback where the UI can't render.
    • "items" — the tappable options. Each id is echoed back on the customer's reply so you know what they picked; each title is 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 under payment. Payment callbacks are relayed to the channel's payment webhook.
  • kind: "authenticate" — put the OAuth2 request under authenticate (or a bare oauth2 object). 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 text and metadata.interactive are 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 kind and populate only that kind's fields.
  • quick_reply must not include received_message / reply_message — send a plain text bubble first.
  • time_picker startTime is ISO-8601 and duration is 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 (one kind) from the schema above as your json_schema, or use one tool per kind. The same schema is served machine-readable in the reference as the interactive field of the send request in openapi.v4.json.