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. Pass from in 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 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 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 (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 — PNGs referenced by imageIdentifier, each { "identifier", "url" } (a public https URL Blooio fetches) or { "identifier", "data" } (base64). Each image must be a real PNG under 200 KB, and all of them together under 5 MB.

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 Apple Messages for Business sender alias. This is the one field that says who the message is from; support resolves 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 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 (identifier works too) comes back as selected.id 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" }
      ]
    }
  ]
}

Each row's identifier (id works too) comes back as selected.id on the reply, so branch on it rather than on the title. A row without one is numbered "1", "2", … within its section.

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": [
      { "identifier": "slot-1", "startTime": "2026-07-20T17:00:00Z", "duration": 3600 },
      { "identifier": "slot-2", "startTime": "2026-07-20T18:00:00Z", "duration": 3600 }
    ]
  }
}

The chosen slot's identifier comes back as selected.id. A slot without one is numbered "1", "2", … in order.

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. The outcome arrives as payment.received, payment.completed, and payment.failed webhook events; see Apple Pay.
  • 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

A reply arrives as a message.received webhook with message_type interactive. It has no plain body, so Blooio fills text with a readable summary (the picked titles or the form answers, joined with commas) and adds an interactive object that says exactly what the customer picked. GET /chats/{chatId}/messages/{messageId} returns the same selection on the message.

Field On What it is
kind Every reply quick_reply, list_picker, time_picker, form, or invitation
chosen Every reply The picked titles in order: option titles, the slot's start time, or the form answers
selected Quick reply, list picker, time picker { "id", "title" } for a single pick. id is the id or identifier you set on the option. A quick reply adds index, the option's position in items
selections List picker An array of { "id", "title" } in place of selected when the customer picked more than one row
responses Form One entry per answered page: page_identifier, question, and answers (identifier, type, title, value)
request_identifier Webhook The request_identifier you sent, or the request message's id without its msg_ prefix
detail Webhook Apple's raw reply object for the kind
role, label REST role: "reply" and a display label such as List Picker

Branch on selected.id, or on each selections[].id. Titles are for display: they change when you reword an option, and text is only a summary.

app.post('/webhooks/blooio', (req, res) => {
  const { type, data } = req.body;
  const reply = data.interactive;
  if (type === 'message.received' && data.channel_type === 'amb' && reply) {
    const picks = reply.selections ?? (reply.selected ? [reply.selected] : []);
    for (const { id } of picks) addToCart(data.chat_id, id);
  }
  res.sendStatus(200);
});

Quick reply

{
  "id": "evt_018f7b2d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": 1718000000000,
  "organization_id": "org_abc123",
  "data": {
    "message_id": "msg_018f7b2d-6f7a-7b8c-9d0e-2f3a4b5c6d7e",
    "chat_id": "chat_018f7b2d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "channel_id": "ch_018f7b2d-0f1e-7d2c-9b3a-4c5d6e7f8a9b",
    "channel_type": "amb",
    "kind": "received",
    "direction": "inbound",
    "text": "Track an order",
    "status": "received",
    "protocol": "amb",
    "message_type": "interactive",
    "reply_to_message_id": null,
    "sender": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
    "recipient": "Driftwood Home",
    "contact": {
      "contact_id": "ct_018f7b2d-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
      "identifier": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
      "name": null
    },
    "group": null,
    "channel_address": "Driftwood Home",
    "attachments": [],
    "locale": "en_US",
    "interactive": {
      "kind": "quick_reply",
      "request_identifier": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a9f",
      "chosen": ["Track an order"],
      "selected": { "id": "track", "index": 0, "title": "Track an order" },
      "detail": {
        "items": [
          { "title": "Track an order", "identifier": "track" },
          { "title": "Start a return", "identifier": "return" }
        ],
        "selectedIndex": 0,
        "selectedIdentifier": "track"
      }
    }
  }
}

List picker

Apple sends the whole list back: the picked rows in a first section titled "You Selected" (in the customer's language) and every other row under "Others". selected reads that for you:

{
  "id": "evt_018f7b2e-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": 1718000000000,
  "organization_id": "org_abc123",
  "data": {
    "message_id": "msg_018f7b2e-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
    "chat_id": "chat_018f7b2d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "channel_id": "ch_018f7b2d-0f1e-7d2c-9b3a-4c5d6e7f8a9b",
    "channel_type": "amb",
    "kind": "received",
    "direction": "inbound",
    "text": "Oak dining table",
    "status": "received",
    "protocol": "amb",
    "message_type": "interactive",
    "reply_to_message_id": null,
    "sender": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
    "recipient": "Driftwood Home",
    "contact": {
      "contact_id": "ct_018f7b2d-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
      "identifier": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
      "name": null
    },
    "group": null,
    "channel_address": "Driftwood Home",
    "attachments": [],
    "locale": "en_US",
    "interactive": {
      "kind": "list_picker",
      "request_identifier": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
      "chosen": ["Oak dining table"],
      "selected": { "id": "sku_oak_table", "title": "Oak dining table" },
      "detail": {
        "sections": [
          {
            "order": 0,
            "title": "You Selected",
            "multipleSelection": false,
            "items": [
              {
                "order": 0,
                "style": "default",
                "title": "Oak dining table",
                "subtitle": "Seats 6 · $1,240",
                "identifier": "sku_oak_table"
              }
            ]
          },
          {
            "order": 1,
            "title": "Others",
            "multipleSelection": false,
            "items": [
              {
                "order": 0,
                "style": "default",
                "title": "Walnut sideboard",
                "subtitle": "3 drawers · $890",
                "identifier": "sku_walnut_sideboard"
              },
              {
                "order": 1,
                "style": "default",
                "title": "Linen armchair",
                "subtitle": "4 colors · $560",
                "identifier": "sku_linen_chair"
              }
            ]
          }
        ]
      }
    }
  }
}

When a section sets multipleSelection and the customer picks several rows, selections replaces selected. Apple reports multipleSelection: false in detail on every reply, so read selections rather than that flag:

{
  "id": "evt_018f7b2e-3c4d-7e5f-8a6b-7c8d9e0f1a2b",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": 1718000000000,
  "organization_id": "org_abc123",
  "data": {
    "message_id": "msg_018f7b2e-4d5e-7f6a-8b7c-8d9e0f1a2b3c",
    "chat_id": "chat_018f7b2d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "channel_id": "ch_018f7b2d-0f1e-7d2c-9b3a-4c5d6e7f8a9b",
    "channel_type": "amb",
    "kind": "received",
    "direction": "inbound",
    "text": "Oak dining table, Linen armchair",
    "status": "received",
    "protocol": "amb",
    "message_type": "interactive",
    "reply_to_message_id": null,
    "sender": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
    "recipient": "Driftwood Home",
    "contact": {
      "contact_id": "ct_018f7b2d-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
      "identifier": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
      "name": null
    },
    "group": null,
    "channel_address": "Driftwood Home",
    "attachments": [],
    "locale": "en_US",
    "interactive": {
      "kind": "list_picker",
      "request_identifier": "6e5d4c3b-2a1f-4e0d-9c8b-7a6f5e4d3c2b",
      "chosen": ["Oak dining table", "Linen armchair"],
      "selections": [
        { "id": "sku_oak_table", "title": "Oak dining table" },
        { "id": "sku_linen_chair", "title": "Linen armchair" }
      ],
      "detail": {
        "sections": [
          {
            "order": 0,
            "title": "You Selected",
            "multipleSelection": false,
            "items": [
              {
                "order": 0,
                "style": "default",
                "title": "Oak dining table",
                "subtitle": "Seats 6 · $1,240",
                "identifier": "sku_oak_table"
              },
              {
                "order": 1,
                "style": "default",
                "title": "Linen armchair",
                "subtitle": "4 colors · $560",
                "identifier": "sku_linen_chair"
              }
            ]
          },
          {
            "order": 1,
            "title": "Others",
            "multipleSelection": false,
            "items": [
              {
                "order": 0,
                "style": "default",
                "title": "Walnut sideboard",
                "subtitle": "3 drawers · $890",
                "identifier": "sku_walnut_sideboard"
              }
            ]
          }
        ]
      }
    }
  }
}

Time picker

chosen and selected.title are the slot's start time in Apple's format (2026-07-21T17:00+0000). duration is in seconds:

{
  "id": "evt_018f7b2e-5e6f-7a7b-8c8d-9e0f1a2b3c4d",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": 1718000000000,
  "organization_id": "org_abc123",
  "data": {
    "message_id": "msg_018f7b2e-6f7a-7b8c-9d0e-0f1a2b3c4d5e",
    "chat_id": "chat_018f7b2d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "channel_id": "ch_018f7b2d-0f1e-7d2c-9b3a-4c5d6e7f8a9b",
    "channel_type": "amb",
    "kind": "received",
    "direction": "inbound",
    "text": "2026-07-21T17:00+0000",
    "status": "received",
    "protocol": "amb",
    "message_type": "interactive",
    "reply_to_message_id": null,
    "sender": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
    "recipient": "Driftwood Home",
    "contact": {
      "contact_id": "ct_018f7b2d-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
      "identifier": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
      "name": null
    },
    "group": null,
    "channel_address": "Driftwood Home",
    "attachments": [],
    "locale": "en_US",
    "interactive": {
      "kind": "time_picker",
      "request_identifier": "7f6e5d4c-3b2a-4f1e-8d0c-9b8a7f6e5d4c",
      "chosen": ["2026-07-21T17:00+0000"],
      "selected": { "id": "slot-2", "title": "2026-07-21T17:00+0000" },
      "detail": {
        "identifier": "design-consult",
        "title": "Design consultation",
        "timeslots": [
          { "identifier": "slot-2", "startTime": "2026-07-21T17:00+0000", "duration": 1800 }
        ]
      }
    }
  }
}

Form

responses has one entry per page the customer answered; pages left blank are skipped. title is the answer as the customer saw it and value is the raw value, such as an ISO timestamp for a date. type is the control that collected it: input, select, picker, or datePicker:

{
  "id": "evt_018f7b2e-7a8b-7c9d-8e0f-1a2b3c4d5e6f",
  "type": "message.received",
  "api_version": "2026-10-01",
  "created_at": 1718000000000,
  "organization_id": "org_abc123",
  "data": {
    "message_id": "msg_018f7b2e-8b9c-7d0e-9f1a-2b3c4d5e6f7a",
    "chat_id": "chat_018f7b2d-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "channel_id": "ch_018f7b2d-0f1e-7d2c-9b3a-4c5d6e7f8a9b",
    "channel_type": "amb",
    "kind": "received",
    "direction": "inbound",
    "text": "J. Appleseed, Delivery, 07/30/2026",
    "status": "received",
    "protocol": "amb",
    "message_type": "interactive",
    "reply_to_message_id": null,
    "sender": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
    "recipient": "Driftwood Home",
    "contact": {
      "contact_id": "ct_018f7b2d-2b3c-7d4e-8f5a-6b7c8d9e0f1a",
      "identifier": "urn:mbid:AQAAY0vHZ7w2kQnB4xT1Ld9sXq8RmPzEoFhUj3yNcVt6aWb5",
      "name": null
    },
    "group": null,
    "channel_address": "Driftwood Home",
    "attachments": [],
    "locale": "en_US",
    "interactive": {
      "kind": "form",
      "request_identifier": "booking-42",
      "chosen": ["J. Appleseed", "Delivery", "07/30/2026"],
      "responses": [
        {
          "page_identifier": "details",
          "question": "Your details",
          "answers": [
            {
              "identifier": "name",
              "type": "input",
              "title": "J. Appleseed",
              "value": "J. Appleseed"
            }
          ]
        },
        {
          "page_identifier": "method",
          "question": "Pickup or delivery?",
          "answers": [
            { "identifier": "delivery", "type": "select", "title": "Delivery", "value": "delivery" }
          ]
        },
        {
          "page_identifier": "when",
          "question": "Preferred date",
          "answers": [
            {
              "identifier": "date",
              "type": "datePicker",
              "title": "07/30/2026",
              "value": "2026-07-30T07:00:00Z"
            }
          ]
        }
      ],
      "detail": {
        "version": "1.2",
        "template": "messageForms",
        "selections": [
          {
            "pageIdentifier": "details",
            "title": "Your details",
            "subtitle": "",
            "items": [
              {
                "identifier": "name",
                "type": "input",
                "title": "J. Appleseed",
                "value": "J. Appleseed"
              }
            ]
          },
          {
            "pageIdentifier": "method",
            "title": "Pickup or delivery?",
            "subtitle": "",
            "items": [
              {
                "identifier": "delivery",
                "type": "select",
                "title": "Delivery",
                "value": "delivery"
              }
            ]
          },
          {
            "pageIdentifier": "when",
            "title": "Preferred date",
            "subtitle": "",
            "items": [
              {
                "identifier": "date",
                "type": "datePicker",
                "title": "07/30/2026",
                "value": "2026-07-30T07:00:00Z"
              }
            ]
          }
        ]
      }
    }
  }
}

Outcomes that arrive as other events

  • Apple Pay. The Paid bubble in the thread doesn't send a webhook. The payment arrives as payment.received, then payment.completed; see Apple Pay.
  • Sign in. The result arrives as identity.verified; see Sign in.
  • Business invitations. Accepting arrives as a message.received with kind: "invitation", and declining closes the chat with chat.closed; see Business invitations.

Note Large replies (common for multi-page forms and list pickers with images) arrive from Apple as a reference that Blooio resolves after acknowledging the inbound. The message text and interactive are backfilled once resolved, so the webhook fires with the complete reply.

Capabilities

A device advertises which interactive features it supports. GET /chats/{chatId} returns them as capabilities.amb_capability_list (for example ["QUICK", "LIST", "TIME", "FORM", "AUTH2"]) and, more usefully, as supported_interactive_kinds: the kind values you can send to this customer right now. apple_pay and imessage_app are always listed, and every kind is listed until the customer's device has reported its capabilities. Sending a kind the device can't render fails with 422 amb_capability_unavailable, unless you set fallback to downgrade it; see the fallback decision tree.

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 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.