Skip to content
Linq

Preview a message

POST/v1/chats/{chat}/messages/preview

Previews a send without sending it. Use it to surface field-level errors and capability rewrites in a composer before anything reaches the customer.

Behavior

  • Runs the identical accept pipeline as the send: the same validation, chat gates, degrade decision, and consent policy, but records nothing: no message id is minted, no idempotency claim is made, and no Idempotency-Key is required.
  • A body this endpoint refuses, the send refuses with the same code and param, except that a source_url attachment is not fetched during preview. Its reachability, MIME, byte size and storage readiness are therefore send-time facts, and the preview returns the unresolved source form rather than an att_id. Otherwise a body preview accepts is accepted by send, chat state permitting, since a preview cannot freeze the chat between the two calls.

Limits

  • The per-message wire field is representative, never exact; wire_fidelity states this in the response. Values resolved only at delivery are substituted or omitted: payment sessions and app identities appear as placeholders, image bytes are omitted, and a plain attachment message has no wire at all. Do not present it as the exact payload Apple will receive.
Path ParametersExpand Collapse
chat: string
Query ParametersExpand Collapse
force: optional boolean

Same meaning as on the send: true previews the send as an override of the exclusive-owner gate.

Body ParametersJSONExpand Collapse
parts: array of TextPart { body, type, subject } or ChoicesPart { items, summary_text, type } or ListPickerPart { sections, summary_text, type, 4 more } or 8 more

The message content, ≤20 parts. Structural rule (adapter L3): at most one interactive part per message, and text may not be combined with an interactive part; send two messages. An empty array returns HTTP 422 code 1052.

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.

SourceAttachmentPart object { name, source_url, type }

A partner-hosted attachment imported synchronously before the send is accepted. Linq stores a frozen copy; later source changes cannot alter delivery. The URL must be public HTTPS on port 443 and require no headers, cookies or authentication. Redirects are accepted only when every hop passes the same secure URL policy.

name: string

Filename delivered to the customer.

minLength1
source_url: string

Public HTTPS URL fetched before acceptance.

maxLength2048
type: "attachment"
category: optional "transactional" or "account_notification" or "marketing"

Apple notification category. OPTIONAL by contract: absent means transactional (an in-chat reply): a bare {"parts":[…]} must keep working bit-for-bit. Unknown value → 422 code 1010. A non-transactional send with no valid opt-in on file returns HTTP 403 code 2015 (force=true does NOT override consent).

One of the following:
"transactional"
"account_notification"
"marketing"
degrade: optional "reject" or "auto" or "acknowledged"

Capability fallback opt-in. Absent or reject (the default): an unsupported part type is refused 409 code 4005. auto: the part is rewritten to the best representation the device renders, and the response reports each swap it made in fallbacks. acknowledged: auto, plus the statement that a HUMAN has seen what the downgrade produces and accepted it; identical to auto for an automated caller, and the ONLY value that lets a human-attributed send be rewritten (a send attributed to a named operator that auto would rewrite is refused 409 code 4005, outright rather than warned about). Any other value → 422 code 1091.

One of the following:
"reject"
"auto"
"acknowledged"
typing_lead_ms: optional number

How long the typing indicator runs before this message, in MILLISECONDS.

THE INDICATOR IS YOURS TO ASK FOR. We show one for exactly as long as you request and never on our own, so this field is the whole control: absent or 0 shows no indicator at all, and a positive value shows one for precisely that long before the message reaches the customer.

OPTIONAL by contract, and absent means the same as 0: no indicator. We recommend asking for about 1000 ms before each message you send, and longer when the customer is about to wait on a lookup or a handoff, so the pause reads as thinking rather than as a stall.

Bounded at 15000 (422 code 1096, which is also what a negative value gets). The cap is derived from the budget one send attempt gets: a longer indicator eats the time the message send itself needs, so the send would be cut short and retried and the customer would watch a long indicator followed by a late message or none.

formatint32
minimum0
maximum15000
ReturnsExpand Collapse
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

Preview a message

curl https://messages.api.linqapp.com/v1/chats/$CHAT/messages/preview \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{
          "parts": [
            {
              "items": [
                {
                  "id": "curb",
                  "label": "Curbside"
                },
                {
                  "id": "garage",
                  "label": "Garage level 2"
                }
              ],
              "summary_text": "Pickup options",
              "type": "choices"
            }
          ]
        }'
{
  "chat_id": "chat_4f81b2",
  "wire_fidelity": "representative",
  "messages": [
    {
      "type": "choices",
      "parts": [
        {
          "type": "choices",
          "summary_text": "Pickup options",
          "items": [
            {
              "id": "curb",
              "label": "Curbside"
            },
            {
              "id": "garage",
              "label": "Garage level 2"
            }
          ]
        }
      ]
    }
  ]
}
Returns Examples
{
  "chat_id": "chat_4f81b2",
  "wire_fidelity": "representative",
  "messages": [
    {
      "type": "choices",
      "parts": [
        {
          "type": "choices",
          "summary_text": "Pickup options",
          "items": [
            {
              "id": "curb",
              "label": "Curbside"
            },
            {
              "id": "garage",
              "label": "Garage level 2"
            }
          ]
        }
      ]
    }
  ]
}