## Send a message

**post** `/v1/chats/{chat}/messages`

Sends a message into a chat.

**Requirements**

- `Idempotency-Key` header: required. A UUID you mint per logical send.
- The chat must be open, and you must hold the send turn (see Chat
  control).

**Behavior**

- Replaying the same `Idempotency-Key` within 24 hours returns the
  original response. A refused request records no key, so the same key
  succeeds on retry.
- An `https` URL in message text may be promoted to its own rich-link
  message. Each resulting message has its own `message.sent` or
  `message.failed` lifecycle, correlated by `group_id`.
- Rate limiting is per brand: only your own traffic can produce it, and
  `Retry-After` is computed from your brand's own refill rate. A
  rate-limited request is not validated: an invalid send still fails
  with its own error once you are back within rate. A request that fans
  out into several messages costs one rate credit per message.

**Errors**

- HTTP 400: `idempotency_key_required`, `body_invalid`
- HTTP 403: `consent_required`
- HTTP 409: `chat_closed`, `chat_owned`, `capability_unsupported`,
  `idempotency_key_reused`, `idempotency_key_processing` while another
  request with the same key is still in flight
- HTTP 413: `attachment_source_too_large`
- HTTP 422: field-level validation (`category_invalid`, `degrade_invalid`,
  `attachment_source_invalid`, `attachment_source_empty`, and per-part
  codes), each naming the exact field at fault
- HTTP 429: `rate_limited`, with `Retry-After`
- HTTP 502: `attachment_source_unavailable`
- HTTP 503: `send_paused`, `attachment_source_busy` (with `Retry-After`),
  or `attachment_storage_unavailable`

### Path Parameters

- `chat: string`

### Query Parameters

- `force: optional boolean`

  `true` bypasses the exclusive-owner gate (sends while
  `owner != partner`). A real override of a non-partner owner is audited
  (`chat.send_forced`). Does NOT override consent policy.

### Header Parameters

- `"Idempotency-Key": string`

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

- `id: string`

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

- `chat_id: string`

- `delivery: "queued"`

  - `"queued"`

- `messages: array of SentMessage`

  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.

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

  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`

    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 \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_AMB_API_KEY" \
    -d '{
          "parts": [
            {
              "body": "Your driver Marta is two minutes away in a blue Prius.",
              "type": "text"
            }
          ]
        }'
```

#### Response

```json
{
  "id": "msg_2c7d90",
  "chat_id": "chat_4f81b2",
  "delivery": "queued",
  "messages": [
    {
      "id": "msg_2c7d90",
      "type": "text"
    }
  ]
}
```
