Skip to content
Linq

Messages

Request

POST /v1/chats/{chat}/messages
Authorization: Bearer sk_live_...
Idempotency-Key: 3f2504e0-4f89-11d3-9a0c-0305e82c3301

{"parts": [{"type": "text", "body": "Your order ships tomorrow."}]}

Response

{"id": "msg_4a1c...",
 "chat_id": "chat_2d4f8a91...",
 "delivery": "queued",
 "messages": [{"id": "msg_4a1c...", "type": "text"}]}

Idempotency-Key is required. A replay of the same key within 24 hours returns the original response with Idempotent-Replayed: true. A refused send records no key, so the same key can be reused once the cause is fixed.

Delivery

queued means accepted, not delivered. The outcome arrives as message.sent or message.failed, or from GET /v1/messages/{msg} if you missed the webhook. message.failed carries a reason; consent_revoked, capability_rejected, and so on.

One request, several messages

An https:// URL in a text part becomes a rich link, and the text around it becomes a message of its own. So one request can produce several messages.

They all appear in messages, they are all covered by the one Idempotency-Key, and each one gets its own delivery webhook, tied together by group_id. Count what’s in messages; not the requests you sent.

Parts

A message is an array of parts. Ten types are available:

text · choices · list_picker · time_slots · form · rich_link · attachment · apple_pay · authenticate · app_extension

Each type has a JSON Schema at GET /v1/parts/{type}. We serve it unmodified, so you can hand it straight to a structured-output model as a tool schema.

Some rules can’t be expressed in JSON Schema; length caps, how many choices a picker may carry, whether an ISO code is upper or lower case. Those ride along as metadata on each schema, and we enforce them when you send. Breaking one returns 422 with a pointer to the exact field at fault.

Text plus attachments is the only multi-part combination Apple accepts. The character (U+FFFC) positions each attachment within the text.

Sending a file

Send the file and the message in one request: make the POST multipart/form-data, put the message JSON in a message field, and give each attachment part the name of the file part carrying its bytes.

POST /v1/chats/{chat}/messages
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Content-Type: multipart/form-data

message = {"parts": [
    {"type": "text",       "body": "Here's the menu you asked for"},
    {"type": "attachment", "file": "photo"}
  ], "category": "transactional"}
photo = <the JPEG bytes>

The response is the same as any send. A blind retry with the same Idempotency-Key and the same file replays rather than re-sending. Max size is 100 MB; per file, and for a message’s files together.

Artwork you reference by id

Some parts name a stored file instead of carrying one, and they take an attachment id rather than inline base64: a rich link’s image_att_id, the images inside a list picker, a time-slot picker or a form, and your brand’s default rich-link image.

Upload the asset once, then reference that id as often as you need it.

POST /v1/attachments
Content-Type: image/png
x-filename: menu-thumb.png

<the PNG bytes>
{"id": "att_1a3f77", "mime": "image/png", "size": 20481}

Uploads are idempotent over your brand and the bytes, so a retry returns the same id and stores nothing twice. The stored mime and name stay the first upload’s, so uploading the same bytes under a corrected content-type hands you the original type back; to change a stored type, upload altered bytes, which are a different attachment.

This route is for assets you reference. To send a file to a customer, put it in the send, as above.

Capabilities

Each inbound message announces what the customer’s device can render, most recent wins. Sending a part the current device cannot render returns:

Response

{"error": {"type": "capability", "code": "capability_unsupported",
           "message": "this device did not announce FORM"},
 "request_id": "req_01HQ7X..."}

Set "degrade": "auto" and we rewrite the part into something the device can display, then return a fallbacks array describing what we swapped. If fallbacks is present, the customer saw something other than what you composed; check it before assuming your message landed as written.

Limits

Attachment100 MB; exactly 100,000,000 bytes
Attachments per message100,000,000 bytes across all of them, together
Serialized interactiveData10 KiB
Rich link image200 kB decoded, PNG only
Invitation batch10,000 recipients

The interactiveData limit is measured over your bytes. Content we add at delivery, such as an Apple Pay session or an app_extension icon, is not charged against it.

The two attachment rows are the same number for a reason: one 100 MB file is fine, and so are ten 10 MB files, but a message carrying more than 100,000,000 bytes of files in total is more than we will carry to Apple in one send. Files sent in the request are refused up front with 413 too_large naming the ceiling.


Send a message
POST/v1/chats/{chat}/messages
Preview a message
POST/v1/chats/{chat}/messages/preview
ModelsExpand Collapse
AppExtensionPart object { app, received_message, reply_message, 4 more }

A third-party App Extension bubble. We fill in the app identity (bid, appId, appName, appIcon) from your brand’s registered apps, so those are rejected as request fields. We never read url; it is handed to the extension exactly as you sent it.

app: string

Which of the brand’s registered apps to send under.

received_message: object { title, subtitle }

Shown on a device without the extension installed.

title: string
subtitle: optional string
reply_message: object { title, subtitle }

Shown on a device without the extension installed.

title: string
subtitle: optional string
type: "app_extension"
url: string

The payload the Messages app hands to the extension. Opaque: never parsed or rewritten.

session_id: optional string

Reuse to address a card already sent; omit and Apple generates one.

use_live_layout: optional boolean

Defaults to true.

ApplePayPart object { payment_request, received_message, type }

AMB Apple Pay payment request. We fill in the merchant identity, gateway URL and merchant session from your brand’s configuration, so merchant_identifier, endpoints and merchant_session are rejected as unknown fields. Amounts are decimal strings.

payment_request: object { country_code, currency_code, line_items, 7 more }
country_code: string
currency_code: string
line_items: array of object { amount, label, type }
amount: string
label: string
type: optional "final" or "pending" or ""

Empty means unset.

One of the following:
"final"
"pending"
""
merchant_capabilities: array of "supports3DS" or "supportsCredit" or "supportsDebit" or "supportsEMV"
One of the following:
"supports3DS"
"supportsCredit"
"supportsDebit"
"supportsEMV"
supported_networks: array of "amex" or "discover" or "jcb" or 3 more
One of the following:
"amex"
"discover"
"jcb"
"masterCard"
"privateLabel"
"visa"
total: object { amount, label, type }

The grand total.

amount: string
label: string
type: optional "final" or "pending" or ""

Empty means unset.

One of the following:
"final"
"pending"
""
required_billing_contact_fields: optional array of "email" or "name" or "phone" or 2 more
One of the following:
"email"
"name"
"phone"
"phoneticName"
"post"
required_shipping_contact_fields: optional array of "email" or "name" or "phone" or 2 more
One of the following:
"email"
"name"
"phone"
"phoneticName"
"post"
shipping_methods: optional array of object { amount, detail, identifier, label }
amount: string
detail: string
identifier: string
label: string
supported_countries: optional array of string
received_message: object { title, image_att_id, subtitle }

The bubble the customer sees before authorizing.

title: string
image_att_id: optional string
subtitle: optional string
type: "apple_pay"
AttachmentPart object { att_id, type }

An attachment by reference: att_id names a file the platform holds for your brand, such as an inbound attachment we retained (the id on the message.received event). To send a file you host, use this part’s source_url form instead; never inline base64.

att_id: string
type: "attachment"
AuthenticatePart object { received_title, reply_title, scope, 3 more }

Opens Apple’s sign-in sheet (New Authentication Message). You supply the OAuth scope and the two bubble titles. We fill in state and redirect_uri when the customer accepts.

received_title: string

The bubble the customer sees.

reply_title: string

The bubble after completion.

scope: array of string
type: "authenticate"
redirect_uri: optional string
state: optional string
ChoicesPart object { items, summary_text, type }

AMB Quick Reply: single-select items. summary_text is the transcript label, not the prompt; send the prompt as a preceding text message.

items: array of object { id, label }
id: string
label: string
summary_text: string
type: "choices"
Fallback object { token, from, rule, 2 more }

One capability rewrite performed under degrade: "auto", or "acknowledged", which rewrites on exactly the same rules and records the swap the same way.

token: string

The missing capability token that forced the rewrite.

from: string

Canonical part type in.

rule: string

The fallback-tree rule applied (e.g. guide-17.5-select-le5).

to: string

Canonical part type out. For a rewritten form this is text_sequence: the whole degraded fan is one rewrite outcome, stable across how many messages it produced; which types those messages carry is on the response’s own messages[].

options: optional array of FallbackOption { index, item_id, label }

Present only when the rewrite flattened a menu.

index: number

1-based, matching the numbering the degraded copy emits.

formatint32
item_id: string

The original item id, so a customer’s “2” maps back to your routing.

label: string
FallbackOption object { index, item_id, label }

One entry of a menu a degrade rewrite flattened into words.

index: number

1-based, matching the numbering the degraded copy emits.

formatint32
item_id: string

The original item id, so a customer’s “2” maps back to your routing.

label: string
FormPart object { header, pages, type, 7 more }

AMB Message Form (dynamic v1.2). Page kinds: select, picker, datePicker, input. The last page is the submit page. Omitting show_summary means true.

header: string
pages: array of object { id, kind, subtitle, 15 more }
id: string
kind: "select" or "picker" or "datePicker" or "input"
One of the following:
"select"
"picker"
"datePicker"
"input"
subtitle: string

The question shown to the user.

date_format: optional string

datePicker only. ICU pattern, default MM/dd/yyyy. It governs how the device reads the three date strings.

hint_text: optional string

datePicker only.

items: optional array of object { id, label, image_id }

select and picker only.

id: string
label: string
image_id: optional string

select items only.

keyboard_type: optional "default" or "asciiCapable" or "numbersAndPunctuation" or 7 more

input only.

One of the following:
"default"
"asciiCapable"
"numbersAndPunctuation"
"URL"
"numberPad"
"phonePad"
"namePhonePad"
"emailAddress"
"decimalPad"
"webSearch"
label_text: optional string

datePicker only; defaults to “Date”.

max_chars: optional number

input only.

maximum_date: optional string

datePicker only, in date_format.

minimum_date: optional string

datePicker only, in date_format.

multiple: optional boolean

select only.

next_page_id: optional string
picker_title: optional string

picker only; empty centers the field.

required: optional boolean

input only.

selected_item_index: optional number

picker only.

start_date: optional string

datePicker only, in date_format.

title: optional string
type: "form"
bubble_image_id: optional string

Icon on the received/reply message bubble.

bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

show_summary: optional boolean
splash_button_title: optional string

Defaults to “Start”. The splash view shows when any splash field is set.

splash_image_id: optional string
splash_text: optional string
ListPickerPart object { sections, summary_text, type, 4 more }

AMB List Picker: sections of items, per-section multi-select. Item image_id and bubble_image_id reference the shared images pool by id.

sections: array of object { items, title, multi_select }
items: array of object { id, label, detail, image_id }
id: string
label: string
detail: optional string
image_id: optional string
title: string
multi_select: optional boolean
summary_text: string
type: "list_picker"
bubble_image_id: optional string
bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

subtitle: optional string
Part = TextPart { body, type, subject } or ChoicesPart { items, summary_text, type } or ListPickerPart { sections, summary_text, type, 4 more } or 7 more

The sealed part union: one variant per member of the canonical content model (10 types). Discriminated on the wire by each part’s type const.

One of the following:
TextPart object { body, type, subject }

A plain text message. subject renders bold on AMB.

body: string
type: "text"
subject: optional string
ChoicesPart object { items, summary_text, type }

AMB Quick Reply: single-select items. summary_text is the transcript label, not the prompt; send the prompt as a preceding text message.

items: array of object { id, label }
id: string
label: string
summary_text: string
type: "choices"
ListPickerPart object { sections, summary_text, type, 4 more }

AMB List Picker: sections of items, per-section multi-select. Item image_id and bubble_image_id reference the shared images pool by id.

sections: array of object { items, title, multi_select }
items: array of object { id, label, detail, image_id }
id: string
label: string
detail: optional string
image_id: optional string
title: string
multi_select: optional boolean
summary_text: string
type: "list_picker"
bubble_image_id: optional string
bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

subtitle: optional string
TimeSlotsPart object { event, slots, type, 5 more }

AMB Time Picker: whole-minute, future-only slot starts; duration in seconds; timezone offset in minutes.

event: object { title, image_id, location }
title: string
image_id: optional string
location: optional object { latitude, longitude, radius_m, title }
latitude: optional number
longitude: optional number
radius_m: optional number
title: optional string
slots: array of object { id, duration_s, start }
id: string
duration_s: number
start: string
type: "time_slots"
bubble_image_id: optional string
bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

subtitle: optional string
timezone_offset_min: optional number
FormPart object { header, pages, type, 7 more }

AMB Message Form (dynamic v1.2). Page kinds: select, picker, datePicker, input. The last page is the submit page. Omitting show_summary means true.

header: string
pages: array of object { id, kind, subtitle, 15 more }
id: string
kind: "select" or "picker" or "datePicker" or "input"
One of the following:
"select"
"picker"
"datePicker"
"input"
subtitle: string

The question shown to the user.

date_format: optional string

datePicker only. ICU pattern, default MM/dd/yyyy. It governs how the device reads the three date strings.

hint_text: optional string

datePicker only.

items: optional array of object { id, label, image_id }

select and picker only.

id: string
label: string
image_id: optional string

select items only.

keyboard_type: optional "default" or "asciiCapable" or "numbersAndPunctuation" or 7 more

input only.

One of the following:
"default"
"asciiCapable"
"numbersAndPunctuation"
"URL"
"numberPad"
"phonePad"
"namePhonePad"
"emailAddress"
"decimalPad"
"webSearch"
label_text: optional string

datePicker only; defaults to “Date”.

max_chars: optional number

input only.

maximum_date: optional string

datePicker only, in date_format.

minimum_date: optional string

datePicker only, in date_format.

multiple: optional boolean

select only.

next_page_id: optional string
picker_title: optional string

picker only; empty centers the field.

required: optional boolean

input only.

selected_item_index: optional number

picker only.

start_date: optional string

datePicker only, in date_format.

title: optional string
type: "form"
bubble_image_id: optional string

Icon on the received/reply message bubble.

bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

show_summary: optional boolean
splash_button_title: optional string

Defaults to “Start”. The splash view shows when any splash field is set.

splash_image_id: optional string
splash_text: optional string
AttachmentPart object { att_id, type }

An attachment by reference: att_id names a file the platform holds for your brand, such as an inbound attachment we retained (the id on the message.received event). To send a file you host, use this part’s source_url form instead; never inline base64.

att_id: string
type: "attachment"
ApplePayPart object { payment_request, received_message, type }

AMB Apple Pay payment request. We fill in the merchant identity, gateway URL and merchant session from your brand’s configuration, so merchant_identifier, endpoints and merchant_session are rejected as unknown fields. Amounts are decimal strings.

payment_request: object { country_code, currency_code, line_items, 7 more }
country_code: string
currency_code: string
line_items: array of object { amount, label, type }
amount: string
label: string
type: optional "final" or "pending" or ""

Empty means unset.

One of the following:
"final"
"pending"
""
merchant_capabilities: array of "supports3DS" or "supportsCredit" or "supportsDebit" or "supportsEMV"
One of the following:
"supports3DS"
"supportsCredit"
"supportsDebit"
"supportsEMV"
supported_networks: array of "amex" or "discover" or "jcb" or 3 more
One of the following:
"amex"
"discover"
"jcb"
"masterCard"
"privateLabel"
"visa"
total: object { amount, label, type }

The grand total.

amount: string
label: string
type: optional "final" or "pending" or ""

Empty means unset.

One of the following:
"final"
"pending"
""
required_billing_contact_fields: optional array of "email" or "name" or "phone" or 2 more
One of the following:
"email"
"name"
"phone"
"phoneticName"
"post"
required_shipping_contact_fields: optional array of "email" or "name" or "phone" or 2 more
One of the following:
"email"
"name"
"phone"
"phoneticName"
"post"
shipping_methods: optional array of object { amount, detail, identifier, label }
amount: string
detail: string
identifier: string
label: string
supported_countries: optional array of string
received_message: object { title, image_att_id, subtitle }

The bubble the customer sees before authorizing.

title: string
image_att_id: optional string
subtitle: optional string
type: "apple_pay"
AuthenticatePart object { received_title, reply_title, scope, 3 more }

Opens Apple’s sign-in sheet (New Authentication Message). You supply the OAuth scope and the two bubble titles. We fill in state and redirect_uri when the customer accepts.

received_title: string

The bubble the customer sees.

reply_title: string

The bubble after completion.

scope: array of string
type: "authenticate"
redirect_uri: optional string
state: optional string
AppExtensionPart object { app, received_message, reply_message, 4 more }

A third-party App Extension bubble. We fill in the app identity (bid, appId, appName, appIcon) from your brand’s registered apps, so those are rejected as request fields. We never read url; it is handed to the extension exactly as you sent it.

app: string

Which of the brand’s registered apps to send under.

received_message: object { title, subtitle }

Shown on a device without the extension installed.

title: string
subtitle: optional string
reply_message: object { title, subtitle }

Shown on a device without the extension installed.

title: string
subtitle: optional string
type: "app_extension"
url: string

The payload the Messages app hands to the extension. Opaque: never parsed or rewritten.

session_id: optional string

Reuse to address a card already sent; omit and Apple generates one.

use_live_layout: optional boolean

Defaults to true.

PreviewMessageResponse object { chat_id, messages, wire_fidelity, 2 more }

The previewed send. Nothing was written; nothing will be sent.

chat_id: string
messages: array of PreviewedMessage { parts, type, wire, wire_unavailable }

What the ONE request would become, message by message (URL promotion can make several).

parts: array of unknown

The message’s post-degrade, post-promotion parts. A URL attachment remains in its source form because preview does not fetch it or mint the real att_id that an accepted send returns. Typed as an opaque array rather than Part[] for the same one-decode-door reason TranscriptRow.parts is.

type: string

The part type of this message’s FIRST part: the same value the send’s 200 reports per message.

wire: optional unknown

The representative Apple wire fields for this message; see the operation description for exactly what is substituted. Absent when wire_unavailable says why.

wire_unavailable: optional string

Present only when this message has no wire preview (a plain attachment message), and why.

wire_fidelity: "representative"

Always representative: the wire bytes are an approximation of what delivery sends, never the exact payload.

capture: optional object { collection, expects, page_id, 4 more }

The reply capture this send WOULD open, identical to the send 200’s object except group_id, which is ABSENT here (nothing was minted). Absent entirely when no capture would open.

collection: string

single_shot: the whole prompt sequence goes at once. progressive is reserved.

expects: string

The reply shape we will match: choice, multi_choice, text, phone, email, or date_text (a date question at the text floor records the customer’s words verbatim; date_choice is reserved for a future variant that offers concrete slots).

page_id: string

Your form page’s own id: the key the eventual reply.values uses.

reasks: number

How many re-asks a mismatched reply earns before the collection ends abandoned. Fixed at 1.

formatint32
timeout_s: number

How long the collection stays open, in seconds. Fixed at 1800.

formatint32
group_id: optional string

ABSENT on the preview (nothing was minted); on the send 200, the fan’s correlation id: the primary message’s own id, the value form.response reports back.

options: optional array of object { index, item_id, label }

Present for the choice shapes only: what the customer can answer with, by number or label.

index: number
formatint32
item_id: string
label: string
fallbacks: optional array of Fallback { token, from, rule, 2 more }

The capability rewrites degrade would apply: the same array the send’s 200 would carry. Present only when something would be rewritten.

token: string

The missing capability token that forced the rewrite.

from: string

Canonical part type in.

rule: string

The fallback-tree rule applied (e.g. guide-17.5-select-le5).

to: string

Canonical part type out. For a rewritten form this is text_sequence: the whole degraded fan is one rewrite outcome, stable across how many messages it produced; which types those messages carry is on the response’s own messages[].

options: optional array of FallbackOption { index, item_id, label }

Present only when the rewrite flattened a menu.

index: number

1-based, matching the numbering the degraded copy emits.

formatint32
item_id: string

The original item id, so a customer’s “2” maps back to your routing.

label: string
PreviewedMessage object { parts, type, wire, wire_unavailable }

One message of a previewed send, in send order (URL promotion can make several).

parts: array of unknown

The message’s post-degrade, post-promotion parts. A URL attachment remains in its source form because preview does not fetch it or mint the real att_id that an accepted send returns. Typed as an opaque array rather than Part[] for the same one-decode-door reason TranscriptRow.parts is.

type: string

The part type of this message’s FIRST part: the same value the send’s 200 reports per message.

wire: optional unknown

The representative Apple wire fields for this message; see the operation description for exactly what is substituted. Absent when wire_unavailable says why.

wire_unavailable: optional string

Present only when this message has no wire preview (a plain attachment message), and why.

SentMessage object { id, type, parts }

One accepted message of a send (URL promotion can make several).

id: string

Message id (msg_…).

type: string

The part type of that message’s FIRST part.

parts: optional array of unknown

The message’s canonical parts AS STORED at accept: post-degrade, post-promotion, so you see what the customer will actually receive without a second read. Typed opaque for the same one-decode-door reason TranscriptRow.parts is.

TextPart object { body, type, subject }

A plain text message. subject renders bold on AMB.

body: string
type: "text"
subject: optional string
TimeSlotsPart object { event, slots, type, 5 more }

AMB Time Picker: whole-minute, future-only slot starts; duration in seconds; timezone offset in minutes.

event: object { title, image_id, location }
title: string
image_id: optional string
location: optional object { latitude, longitude, radius_m, title }
latitude: optional number
longitude: optional number
radius_m: optional number
title: optional string
slots: array of object { id, duration_s, start }
id: string
duration_s: number
start: string
type: "time_slots"
bubble_image_id: optional string
bubble_style: optional "icon" or "small" or "large"

Size of the transcript bubble this part renders as, and therefore the size of bubble_image_id inside it. icon is a 280x65 pt bubble with a 40x40 pt image (120x120 px @3x). small is 280x85 pt with 60x60 pt (180x180 px). large is 280x210 pt with a 263x150 pt image (789x450 px), the photo-forward card. Omit to keep this part’s existing bubble.

One of the following:
"icon"
"small"
"large"
images: optional array of object { id, att_id, description }
id: string
att_id: string

A stored attachment’s att_id; never inline base64.

description: optional string

Read aloud by VoiceOver.

subtitle: optional string
timezone_offset_min: optional number
MessageSendResponse object { id, chat_id, delivery, 3 more }

The accepted send. A replay of the same Idempotency-Key within 24h returns these exact bytes; past that window the key is forgotten and the request executes again.

id: string

The FIRST message’s id: the send’s primary id.

chat_id: string
delivery: "queued"
messages: array of SentMessage { id, type, parts }

Every message this request was accepted as, in delivery order; ALWAYS present, one element for an ordinary send. Longer when an https URL in your text was promoted to its own rich-link message: each element then has its own message.sent/message.failed lifecycle, correlated by group_id. More than 10 messages returns HTTP 422 code 1075.

id: string

Message id (msg_…).

type: string

The part type of that message’s FIRST part.

parts: optional array of unknown

The message’s canonical parts AS STORED at accept: post-degrade, post-promotion, so you see what the customer will actually receive without a second read. Typed opaque for the same one-decode-door reason TranscriptRow.parts is.

capture: optional object { collection, expects, page_id, 4 more }

The reply capture this send opened; see CaptureObject. Absent when no capture opened.

collection: string

single_shot: the whole prompt sequence goes at once. progressive is reserved.

expects: string

The reply shape we will match: choice, multi_choice, text, phone, email, or date_text (a date question at the text floor records the customer’s words verbatim; date_choice is reserved for a future variant that offers concrete slots).

page_id: string

Your form page’s own id: the key the eventual reply.values uses.

reasks: number

How many re-asks a mismatched reply earns before the collection ends abandoned. Fixed at 1.

formatint32
timeout_s: number

How long the collection stays open, in seconds. Fixed at 1800.

formatint32
group_id: optional string

ABSENT on the preview (nothing was minted); on the send 200, the fan’s correlation id: the primary message’s own id, the value form.response reports back.

options: optional array of object { index, item_id, label }

Present for the choice shapes only: what the customer can answer with, by number or label.

index: number
formatint32
item_id: string
label: string
fallbacks: optional array of Fallback { token, from, rule, 2 more }

Present ONLY when degrade ("auto" or "acknowledged") actually rewrote a part: its presence is the signal that the customer saw something other than what you composed, and an acknowledgement does not suppress it. Absent otherwise (never an empty array).

token: string

The missing capability token that forced the rewrite.

from: string

Canonical part type in.

rule: string

The fallback-tree rule applied (e.g. guide-17.5-select-le5).

to: string

Canonical part type out. For a rewritten form this is text_sequence: the whole degraded fan is one rewrite outcome, stable across how many messages it produced; which types those messages carry is on the response’s own messages[].

options: optional array of FallbackOption { index, item_id, label }

Present only when the rewrite flattened a menu.

index: number

1-based, matching the numbering the degraded copy emits.

formatint32
item_id: string

The original item id, so a customer’s “2” maps back to your routing.

label: string