## 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 Parameters

- `chat: string`

### Query Parameters

- `force: optional boolean`

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

### Body Parameters

- `parts: array of TextPart or ChoicesPart or ListPickerPart 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.

  - `TextPart object { body, type, subject }`

    A plain text message. subject renders bold on AMB.

    - `body: string`

    - `type: "text"`

      - `"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"`

      - `"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"`

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

      - `"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"`

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

      - `"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"`

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

        - `"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"`

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

      - `"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`

  - `RichLinkPart object { title, type, url, 5 more }`

    A rich link card. Omit image_att_id to get the default card. The image must be a real PNG.

    - `title: string`

    - `type: "rich_link"`

      - `"rich_link"`

    - `url: string`

    - `app_clip: optional boolean`

      Set true when url is an App Clip page, so the App Clip payload is constructed server side (POST /v1/constructPayload). Apple Music and Apple Maps URLs need no flag; they are detected by host. The page must carry App Clip elements and its image must be under 4 MB; if construction fails, the send fails rather than delivering a plain card.

    - `image_att_id: optional string`

    - `store_region: optional string`

      The App Store region Apple resolves the App Clip in, as a 2-letter uppercase ISO-3166 code. Omit for the US, which is Apple's default. Requires app_clip: true; Apple scopes this to App Clips, so setting it on any other rich link is rejected rather than ignored.

    - `video_mime_type: optional string`

      MIME type of the video at video_url. video/mp4 is the recommended container.

    - `video_url: optional string`

      Public https URL of a video asset to play inline in the card. Apple's device fetches this URL client-side when the customer taps play; it must serve the video bytes from the open internet. We never fetch, validate, or re-host them. Requires video_mime_type and image_att_id.

  - `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"`

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

          - `"final"`

          - `"pending"`

          - `""`

      - `merchant_capabilities: array of "supports3DS" or "supportsCredit" or "supportsDebit" or "supportsEMV"`

        - `"supports3DS"`

        - `"supportsCredit"`

        - `"supportsDebit"`

        - `"supportsEMV"`

      - `supported_networks: array of "amex" or "discover" or "jcb" or 3 more`

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

          - `"final"`

          - `"pending"`

          - `""`

      - `required_billing_contact_fields: optional array of "email" or "name" or "phone" or 2 more`

        - `"email"`

        - `"name"`

        - `"phone"`

        - `"phoneticName"`

        - `"post"`

      - `required_shipping_contact_fields: optional array of "email" or "name" or "phone" or 2 more`

        - `"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"`

      - `"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"`

      - `"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"`

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

    - `source_url: string`

      Public HTTPS URL fetched before acceptance.

    - `type: "attachment"`

      - `"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).

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

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

### Returns

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

    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.

    - `"representative"`

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

    - `timeout_s: number`

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

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

      - `item_id: string`

      - `label: string`

  - `fallbacks: optional array of Fallback`

    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`

      Present only when the rewrite flattened a menu.

      - `index: number`

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

      - `item_id: string`

        The original item id, so a customer's "2" maps back to your routing.

      - `label: string`

### Example

```http
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"
            }
          ]
        }'
```

#### Response

```json
{
  "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"
            }
          ]
        }
      ]
    }
  ]
}
```
