# Chats

## List chats

**get** `/v1/chats`

Lists the brand's chats, most recently active first. A chat moves to the
front when its latest event is applied.

The page is a keyset, not an offset: pass the previous page's
`next_cursor` back verbatim. A cursor this API did not issue — including
an empty one — is refused rather than treated as the beginning, so a
paging loop cannot silently restart.

**A walk of this list is not a snapshot, and the gap is one-directional.**
The order is activity and activity only ever moves a chat toward the
front, so a chat that receives a message while you are paging jumps
ABOVE your cursor and is not returned again: on a busy account, a full
walk silently omits the conversations that were active during it. It
never returns one twice.

That is a property of ordering by activity, so plan around it rather
than against it: the chats you missed are exactly the chats that just
had traffic, which `GET /v1/streams/events` announces as it happens and
`GET /v1/chats/{chat}/events` reads back in full. Reconcile with those
two, not by walking the list again. Within one chat there is no such
gap — the transcript's order is the journal's own sequence and never
moves.

### Query Parameters

- `cursor: optional string`

  Cursor from a previous page's `next_cursor`.

- `limit: optional number`

  Page size. The bounds are in this parameter's schema, not only in this sentence — a value outside them is refused, never clamped.

### Returns

- `ChatPage object { data, has_more, next_cursor }`

  A page of chats.

  - `data: array of Chat`

    - `id: string`

      The chat's id.

    - `created_at: string`

    - `customer: string`

      Customer address as an opaque identifier.

    - `customer_window: object { expires_at, state, source }`

      The customer service window — first-class state on every chat read. Only
      `template` parts are sendable while it is closed.

      - `expires_at: string`

        When the open window expires. `null` when no window has ever opened.

      - `state: string`

        `open` or `closed`.

      - `source: optional string`

        What opened the current window: `customer_message`, or `free_entry_point` for ad-sourced windows. Absent when no window has ever opened.

    - `phone_number_id: string`

      The id of the sending number this chat is pinned to.

    - `updated_at: string`

      When the chat last changed — a new event moves it.

    - `from: optional string`

      The number this conversation sends from, in E.164 (leading `+`). This is
      the value to pass back as a send's `from` to address this exact
      conversation. Absent only if the number is no longer one of yours.

  - `has_more: boolean`

    True when another page exists — pass `next_cursor` to fetch it.

  - `next_cursor: optional string`

    Opaque cursor for the next page; present when `has_more` is true.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/chats \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "data": [
    {
      "id": "id",
      "created_at": "2019-12-27T18:11:19.117Z",
      "customer": "customer",
      "customer_window": {
        "expires_at": "2019-12-27T18:11:19.117Z",
        "state": "state",
        "source": "source"
      },
      "phone_number_id": "phone_number_id",
      "updated_at": "2019-12-27T18:11:19.117Z",
      "from": "from"
    }
  ],
  "has_more": true,
  "next_cursor": "next_cursor"
}
```

## Mark a chat read

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

Marks the latest unread inbound message as read, optionally showing the
typing indicator. The upstream command targets that one message; this API
does not promise that the channel also marks earlier inbound messages.
Omit the body, or send `{}`, for read-only; the two forms are identical.
`typing: false` is identical too.

If the chat is not available to this account, the request is refused
with `404 chat_not_found`. If it has no unread inbound message, it is
refused with `409 nothing_unread`; this route does not act as a
standalone typing refresh.

`202` means the command and its `chat.read` journal event are durable;
the upstream action is asynchronous. Read receipts and typing are
outbound-only: this API never reports customer typing.

### Path Parameters

- `chat: string`

### Body Parameters

- `typing: optional boolean`

  Also show the customer the typing indicator, which auto-dismisses after
  a short time or when your next message arrives. Read receipts and typing
  are outbound-only: the API never reports customer typing.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/chats/$CHAT/read \
    -X POST \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

## Get a chat

**get** `/v1/chats/{chat}`

Reads one chat, including its live customer-window state. A chat id that
is not this key's answers `404` — the same answer an id that does not
exist gets, because a response must never confirm one across accounts.

### Path Parameters

- `chat: string`

### Returns

- `Chat object { id, created_at, customer, 4 more }`

  One customer conversation.

  - `id: string`

    The chat's id.

  - `created_at: string`

  - `customer: string`

    Customer address as an opaque identifier.

  - `customer_window: object { expires_at, state, source }`

    The customer service window — first-class state on every chat read. Only
    `template` parts are sendable while it is closed.

    - `expires_at: string`

      When the open window expires. `null` when no window has ever opened.

    - `state: string`

      `open` or `closed`.

    - `source: optional string`

      What opened the current window: `customer_message`, or `free_entry_point` for ad-sourced windows. Absent when no window has ever opened.

  - `phone_number_id: string`

    The id of the sending number this chat is pinned to.

  - `updated_at: string`

    When the chat last changed — a new event moves it.

  - `from: optional string`

    The number this conversation sends from, in E.164 (leading `+`). This is
    the value to pass back as a send's `from` to address this exact
    conversation. Absent only if the number is no longer one of yours.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/chats/$CHAT \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "id": "id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "customer": "customer",
  "customer_window": {
    "expires_at": "2019-12-27T18:11:19.117Z",
    "state": "state",
    "source": "source"
  },
  "phone_number_id": "phone_number_id",
  "updated_at": "2019-12-27T18:11:19.117Z",
  "from": "from"
}
```

## Domain Types

### Chat

- `Chat object { id, created_at, customer, 4 more }`

  One customer conversation.

  - `id: string`

    The chat's id.

  - `created_at: string`

  - `customer: string`

    Customer address as an opaque identifier.

  - `customer_window: object { expires_at, state, source }`

    The customer service window — first-class state on every chat read. Only
    `template` parts are sendable while it is closed.

    - `expires_at: string`

      When the open window expires. `null` when no window has ever opened.

    - `state: string`

      `open` or `closed`.

    - `source: optional string`

      What opened the current window: `customer_message`, or `free_entry_point` for ad-sourced windows. Absent when no window has ever opened.

  - `phone_number_id: string`

    The id of the sending number this chat is pinned to.

  - `updated_at: string`

    When the chat last changed — a new event moves it.

  - `from: optional string`

    The number this conversation sends from, in E.164 (leading `+`). This is
    the value to pass back as a send's `from` to address this exact
    conversation. Absent only if the number is no longer one of yours.

### Chat Event

- `ChatEvent object { occurred_at, seq, type, message }`

  One entry in a chat's gapless event sequence — the transcript is a read of
  these.

  - `occurred_at: string`

  - `seq: number`

    The event's position in this chat's sequence — contiguous from 1, no gaps. Use it as this chat's events-read cursor. It is scoped to one chat, so it is NOT the `/v1/streams/events` stream cursor: that stream spans every chat and carries its own cursor in each frame's `id`.

  - `type: string`

    The event's kind, e.g. `message.received`, `message.sent`. Kinds grow
    additively — skip what you do not handle.

    `chat.event.unknown` is the one kind that never carries meaning: it
    marks a journal entry whose partner-visible kind is not published yet.
    It exists so the sequence stays gapless — an entry you cannot interpret
    is still an entry, and omitting it would put a hole in `seq`. Skip it.

  - `message: optional Message`

    The message this event is about, on message-kind events.

    - `id: string`

      This API's message id. Once outbound reactions are available, put this value in their `message_id`. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

    - `chat_id: string`

      The chat this message belongs to.

    - `created_at: string`

    - `direction: "inbound" or "outbound"`

      `inbound` (from the customer) or `outbound` (sent by you).

      - `"inbound"`

      - `"outbound"`

    - `parts: array of object { body, type, preview_url }  or object { kind, type, caption, 4 more }  or object { language, name, type, 3 more }  or 14 more`

      The message content — sendable parts, plus the inbound-only types on
      inbound messages. Every message carries at least one part.

      - `Text object { body, type, preview_url }`

        A plain text message.

        - `body: string`

          The message text. URLs render as tappable links.

        - `type: "text"`

          - `"text"`

        - `preview_url: optional boolean`

          Render a preview card for the first URL in body. Defaults to false.

      - `Media object { kind, type, caption, 4 more }`

        A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

        - `kind: "image" or "video" or "audio" or 2 more`

          Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

          - `"image"`

          - `"video"`

          - `"audio"`

          - `"document"`

          - `"sticker"`

        - `type: "media"`

          - `"media"`

        - `caption: optional string`

          Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

        - `filename: optional string`

          Display filename. Documents only — a filename on any other kind is rejected, never dropped.

        - `media_id: optional string`

          Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

        - `upload_ref: optional string`

          Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

        - `url: optional string`

          Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

      - `Template object { language, name, type, 3 more }`

        An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template's published parameter schema explicitly names one. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

        - `language: string`

          The template's language-and-locale code, e.g. en_US.

        - `name: string`

          The approved template's name. Lowercase letters, digits and underscores only.

        - `type: "template"`

          - `"template"`

        - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

          Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

          - `card_index: number`

          - `kind: "image" or "video"`

            - `"image"`

            - `"video"`

          - `upload_ref: string`

            A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

          - `buttons: optional array of object { index, payload, type }`

            - `index: number`

            - `payload: string`

            - `type: "quick_reply" or "url"`

              - `"quick_reply"`

              - `"url"`

          - `parameters: optional unknown`

            Named values for this card's body placeholders.

        - `header: optional object { kind, upload_ref }`

          The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

          - `kind: "image" or "video" or "document"`

            - `"image"`

            - `"video"`

            - `"document"`

          - `upload_ref: string`

            A durable handle returned by POST /v1/media.

        - `parameters: optional unknown`

          Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

      - `InteractiveList object { body, button, sections, 3 more }`

        An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

        - `body: string`

          The message body.

        - `button: string`

          Label of the button that opens the list.

        - `sections: array of object { rows, title }`

          The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

          - `rows: array of object { id, title, description }`

            This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

            - `id: string`

              Your identifier for the row — echoed back as the interactive_reply's id.

            - `title: string`

              The row's visible title.

            - `description: optional string`

              Secondary text under the title.

          - `title: optional string`

            Section title. Required when the list has more than one section.

        - `type: "interactive_list"`

          - `"interactive_list"`

        - `footer: optional string`

          Footer text below the body.

        - `header: optional string`

          Header text above the body.

      - `InteractiveButtons object { body, buttons, type, 3 more }`

        An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

        - `body: string`

          The message body.

        - `buttons: array of object { id, title }`

          The tappable buttons — at most three.

          - `id: string`

            Your identifier for the button — echoed back as the interactive_reply's id.

          - `title: string`

            The button's visible label.

        - `type: "interactive_buttons"`

          - `"interactive_buttons"`

        - `footer: optional string`

          Footer text below the body.

        - `header: optional string`

          Header text above the body.

        - `media_header: optional object { kind, upload_ref }`

          Uploaded image, video, or document above the body. Use either header or media_header, never both.

          - `kind: "image" or "video" or "document"`

            - `"image"`

            - `"video"`

            - `"document"`

          - `upload_ref: string`

            A durable handle returned by POST /v1/media.

      - `InteractiveCtaURL object { body, display_text, type, 3 more }`

        An interactive call-to-action message: body text plus one button that opens a URL.

        - `body: string`

          The message body.

        - `display_text: string`

          The button's visible label.

        - `type: "interactive_cta_url"`

          - `"interactive_cta_url"`

        - `url: string`

          The URL the button opens. Must be absolute, with an http or https scheme.

        - `footer: optional string`

          Footer text below the body.

        - `header: optional string`

          Header text above the body.

      - `LocationRequest object { body, type }`

        Asks the customer to share a location. The response arrives as a location part.

        - `body: string`

          Prompt shown above the send-location button.

        - `type: "location_request"`

          - `"location_request"`

      - `AddressRequest object { body, country, type }`

        Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

        - `body: string`

          Prompt shown above the address form.

        - `country: "IN"`

          ISO country code. Structured address requests are currently available only in India.

          - `"IN"`

        - `type: "address_request"`

          - `"address_request"`

      - `Contacts object { contacts, type }`

        One or more contact cards.

        - `contacts: array of object { name, addresses, birthday, 4 more }`

          The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

          - `name: object { formatted_name, first_name, last_name, 3 more }`

            - `formatted_name: string`

              The contact's full display name.

            - `first_name: optional string`

            - `last_name: optional string`

            - `middle_name: optional string`

            - `prefix: optional string`

            - `suffix: optional string`

          - `addresses: optional array of object { city, country, country_code, 4 more }`

            - `city: optional string`

            - `country: optional string`

            - `country_code: optional string`

            - `kind: optional string`

              A label for the address, e.g. WORK, HOME.

            - `state: optional string`

            - `street: optional string`

            - `zip: optional string`

          - `birthday: optional string`

            The contact's birthday as YYYY-MM-DD.

          - `emails: optional array of object { email, kind }`

            - `email: string`

            - `kind: optional string`

              A label for the address, e.g. WORK, HOME.

          - `org: optional object { company, department, title }`

            - `company: optional string`

            - `department: optional string`

            - `title: optional string`

          - `phones: optional array of object { phone, kind, messaging_id }`

            - `phone: string`

              The phone number, ideally in +E.164 form.

            - `kind: optional string`

              A label for the number, e.g. CELL, MAIN, WORK, HOME.

            - `messaging_id: optional string`

              The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

          - `urls: optional array of object { url, kind }`

            - `url: string`

            - `kind: optional string`

              A label for the URL, e.g. WORK, HOME.

        - `type: "contacts"`

          - `"contacts"`

      - `Location object { latitude, longitude, type, 2 more }`

        A location pin.

        - `latitude: number`

          Latitude in decimal degrees.

        - `longitude: number`

          Longitude in decimal degrees.

        - `type: "location"`

          - `"location"`

        - `address: optional string`

          The place's address, shown under the name.

        - `name: optional string`

          The place's name, shown on the pin.

      - `Reaction object { emoji, type, channel_message_id, message_id }`

        An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel's opaque channel_message_id; outbound reactions will carry this API's message_id once sending them becomes available.

        - `emoji: string`

          A single emoji. Send an empty string to remove a previous reaction to the same message.

        - `type: "reaction"`

          - `"reaction"`

        - `channel_message_id: optional string`

          Inbound only: the channel's opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

        - `message_id: optional string`

          Outbound only: this API's Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

      - `InteractiveReply object { id, kind, title, 2 more }`

        Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

        - `id: string`

          The id you assigned to the chosen row or button.

        - `kind: "list_reply" or "button_reply"`

          Which interactive message kind was answered.

          - `"list_reply"`

          - `"button_reply"`

        - `title: string`

          The chosen row's or button's visible title.

        - `type: "interactive_reply"`

          - `"interactive_reply"`

        - `description: optional string`

          The chosen list row's secondary text, when it had one.

      - `AddressReply object { type, values, saved_address_id }`

        Inbound only: the structured values submitted in response to an address request.

        - `type: "address_reply"`

          - `"address_reply"`

        - `values: object { address, building_name, city, 8 more }`

          - `address: optional string`

          - `building_name: optional string`

          - `city: optional string`

          - `floor_number: optional string`

          - `house_number: optional string`

          - `landmark_area: optional string`

          - `name: optional string`

          - `phone_number: optional string`

          - `pin_code: optional string`

          - `state: optional string`

          - `tower_number: optional string`

        - `saved_address_id: optional string`

          Identifier of the selected saved address, when one was selected.

      - `Order object { catalog_id, items, type, text }`

        Inbound only — never sendable. An order the customer placed from a product catalog.

        - `catalog_id: string`

          The catalog the ordered items belong to.

        - `items: array of object { currency, price, product_retailer_id, quantity }`

          The ordered items.

          - `currency: string`

            ISO 4217 currency code for price.

          - `price: number`

            The per-item price at order time.

          - `product_retailer_id: string`

            Your identifier for the product, as registered in the catalog.

          - `quantity: number`

        - `type: "order"`

          - `"order"`

        - `text: optional string`

          Free text the customer attached to the order, when any.

      - `Referral object { type, body, click_id, 7 more }`

        Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

        - `type: "referral"`

          - `"referral"`

        - `body: optional string`

          The ad's body text at click time.

        - `click_id: optional string`

          The click identifier assigned by the ad platform, for attribution.

        - `headline: optional string`

          The ad's headline at click time.

        - `image_url: optional string`

          URL of the ad's image creative, when media_kind is image.

        - `media_kind: optional string`

          The ad creative's media kind, e.g. image or video.

        - `source_id: optional string`

          The ad or post id.

        - `source_kind: optional string`

          What the source was, e.g. ad or post.

        - `source_url: optional string`

          The URL of the ad or post the customer came from.

        - `video_url: optional string`

          URL of the ad's video creative, when media_kind is video.

      - `System object { body, type, event }`

        Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

        - `body: string`

          Human-readable description of the system event.

        - `type: "system"`

          - `"system"`

        - `event: optional string`

          The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

      - `Unsupported object { raw, type, kind }`

        Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

        - `raw: unknown`

          The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

        - `type: "unsupported"`

          - `"unsupported"`

        - `kind: optional string`

          The channel's name for the message kind, when it declares one.

    - `status: string`

      The message's delivery state as last reported.

      Inbound messages read `received`: they arrived, and no delivery of ours
      ran. A message you sent starts `accepted` and moves through `sent`,
      `delivered` and `read` as the channel reports them, with `failed` as the
      definitive negative outcome.

      `unknown` means exactly that: the send is INDETERMINATE and we decline
      to guess. It is NOT terminal — an indeterminate send is never re-sent,
      and it resolves to `sent`, `delivered`, `read` or `failed` when the
      channel's own report arrives — so keep observing rather than treating it
      as an outcome.

      States are reported asynchronously on the chat's event sequence, and the
      set grows additively: treat a value you do not recognise as "no
      information" rather than failing on it.

      Retention of this published status follows the chat transcript policy:
      there is no scheduled pruning during beta, but there is no fixed minimum
      availability guarantee. The present absence of a scheduled age-based
      sweep is not a promise of indefinite availability.

      UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
      `sent` and then `delivered` within milliseconds, every time; `delivered`
      there means a simulator accepted it, not that a device received it. Live
      delivery can lag by hours, can never report `delivered` at all, and can
      fail after the channel accepted the message — so do not calibrate
      timeouts or "delivered means it arrived" logic against a test key.

    - `channel_message_id: optional string`

      The channel's opaque identifier for this message, when the channel has assigned one. Match an inbound reaction's `channel_message_id` to this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier.

    - `failure: optional object { code, message }`

      Why a failed message failed, when we have a reason worth publishing.

      Present only on `status` `failed`, and not on every one of those: not every
      channel refusal has a reason in the published vocabulary, so a message
      that failed on the wire may carry no `failure` at all. Its absence means
      "no published reason", never "no reason".

      `status` stays the thing to branch on. This is additive detail beside it,
      so an integration written before this field existed still sees a terminal
      `failed` and behaves exactly as it did.

      - `code: string`

        The specific reason.

        `sender_deregistered` — the number this message was going out from is no
        longer a registered sending number, and the message never left. This is
        terminal: it is not a pause and it does not clear, so retrying the same
        message cannot succeed and that conversation is over. Reach this customer
        by starting a new one on another of your numbers with a template, exactly
        as you would any customer whose 24-hour window has closed; `GET /v1/chats`
        shows which number each conversation uses. You are not billed for a
        message that failed this way.

        `quality_hold_expired` — this marketing message remained held by an
        automatic safety control on its sending line for ten minutes and was not
        sent. This is terminal: retrying the same message cannot succeed on that
        conversation. Send marketing from another of your numbers with a template,
        or contact support about the sending line. You are not billed for a message
        that failed this way.

      - `message: string`

        Human-readable, written for a person reading it. Do not branch on it.

    - `from: optional string`

      The number this message left by, or arrived on, in E.164 (leading `+`).
      Every message in one conversation carries the same value — a
      conversation never moves to another number. Absent only if the number is
      no longer one of yours.

### Chat Event Page

- `ChatEventPage object { data, has_more, next_cursor }`

  A page of one chat's events, in sequence order.

  - `data: array of ChatEvent`

    - `occurred_at: string`

    - `seq: number`

      The event's position in this chat's sequence — contiguous from 1, no gaps. Use it as this chat's events-read cursor. It is scoped to one chat, so it is NOT the `/v1/streams/events` stream cursor: that stream spans every chat and carries its own cursor in each frame's `id`.

    - `type: string`

      The event's kind, e.g. `message.received`, `message.sent`. Kinds grow
      additively — skip what you do not handle.

      `chat.event.unknown` is the one kind that never carries meaning: it
      marks a journal entry whose partner-visible kind is not published yet.
      It exists so the sequence stays gapless — an entry you cannot interpret
      is still an entry, and omitting it would put a hole in `seq`. Skip it.

    - `message: optional Message`

      The message this event is about, on message-kind events.

      - `id: string`

        This API's message id. Once outbound reactions are available, put this value in their `message_id`. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

      - `chat_id: string`

        The chat this message belongs to.

      - `created_at: string`

      - `direction: "inbound" or "outbound"`

        `inbound` (from the customer) or `outbound` (sent by you).

        - `"inbound"`

        - `"outbound"`

      - `parts: array of object { body, type, preview_url }  or object { kind, type, caption, 4 more }  or object { language, name, type, 3 more }  or 14 more`

        The message content — sendable parts, plus the inbound-only types on
        inbound messages. Every message carries at least one part.

        - `Text object { body, type, preview_url }`

          A plain text message.

          - `body: string`

            The message text. URLs render as tappable links.

          - `type: "text"`

            - `"text"`

          - `preview_url: optional boolean`

            Render a preview card for the first URL in body. Defaults to false.

        - `Media object { kind, type, caption, 4 more }`

          A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

          - `kind: "image" or "video" or "audio" or 2 more`

            Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

            - `"image"`

            - `"video"`

            - `"audio"`

            - `"document"`

            - `"sticker"`

          - `type: "media"`

            - `"media"`

          - `caption: optional string`

            Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

          - `filename: optional string`

            Display filename. Documents only — a filename on any other kind is rejected, never dropped.

          - `media_id: optional string`

            Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

          - `upload_ref: optional string`

            Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

          - `url: optional string`

            Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

        - `Template object { language, name, type, 3 more }`

          An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template's published parameter schema explicitly names one. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

          - `language: string`

            The template's language-and-locale code, e.g. en_US.

          - `name: string`

            The approved template's name. Lowercase letters, digits and underscores only.

          - `type: "template"`

            - `"template"`

          - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

            Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

            - `card_index: number`

            - `kind: "image" or "video"`

              - `"image"`

              - `"video"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

            - `buttons: optional array of object { index, payload, type }`

              - `index: number`

              - `payload: string`

              - `type: "quick_reply" or "url"`

                - `"quick_reply"`

                - `"url"`

            - `parameters: optional unknown`

              Named values for this card's body placeholders.

          - `header: optional object { kind, upload_ref }`

            The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

            - `kind: "image" or "video" or "document"`

              - `"image"`

              - `"video"`

              - `"document"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media.

          - `parameters: optional unknown`

            Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

        - `InteractiveList object { body, button, sections, 3 more }`

          An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

          - `body: string`

            The message body.

          - `button: string`

            Label of the button that opens the list.

          - `sections: array of object { rows, title }`

            The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

            - `rows: array of object { id, title, description }`

              This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

              - `id: string`

                Your identifier for the row — echoed back as the interactive_reply's id.

              - `title: string`

                The row's visible title.

              - `description: optional string`

                Secondary text under the title.

            - `title: optional string`

              Section title. Required when the list has more than one section.

          - `type: "interactive_list"`

            - `"interactive_list"`

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

        - `InteractiveButtons object { body, buttons, type, 3 more }`

          An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

          - `body: string`

            The message body.

          - `buttons: array of object { id, title }`

            The tappable buttons — at most three.

            - `id: string`

              Your identifier for the button — echoed back as the interactive_reply's id.

            - `title: string`

              The button's visible label.

          - `type: "interactive_buttons"`

            - `"interactive_buttons"`

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

          - `media_header: optional object { kind, upload_ref }`

            Uploaded image, video, or document above the body. Use either header or media_header, never both.

            - `kind: "image" or "video" or "document"`

              - `"image"`

              - `"video"`

              - `"document"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media.

        - `InteractiveCtaURL object { body, display_text, type, 3 more }`

          An interactive call-to-action message: body text plus one button that opens a URL.

          - `body: string`

            The message body.

          - `display_text: string`

            The button's visible label.

          - `type: "interactive_cta_url"`

            - `"interactive_cta_url"`

          - `url: string`

            The URL the button opens. Must be absolute, with an http or https scheme.

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

        - `LocationRequest object { body, type }`

          Asks the customer to share a location. The response arrives as a location part.

          - `body: string`

            Prompt shown above the send-location button.

          - `type: "location_request"`

            - `"location_request"`

        - `AddressRequest object { body, country, type }`

          Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

          - `body: string`

            Prompt shown above the address form.

          - `country: "IN"`

            ISO country code. Structured address requests are currently available only in India.

            - `"IN"`

          - `type: "address_request"`

            - `"address_request"`

        - `Contacts object { contacts, type }`

          One or more contact cards.

          - `contacts: array of object { name, addresses, birthday, 4 more }`

            The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

            - `name: object { formatted_name, first_name, last_name, 3 more }`

              - `formatted_name: string`

                The contact's full display name.

              - `first_name: optional string`

              - `last_name: optional string`

              - `middle_name: optional string`

              - `prefix: optional string`

              - `suffix: optional string`

            - `addresses: optional array of object { city, country, country_code, 4 more }`

              - `city: optional string`

              - `country: optional string`

              - `country_code: optional string`

              - `kind: optional string`

                A label for the address, e.g. WORK, HOME.

              - `state: optional string`

              - `street: optional string`

              - `zip: optional string`

            - `birthday: optional string`

              The contact's birthday as YYYY-MM-DD.

            - `emails: optional array of object { email, kind }`

              - `email: string`

              - `kind: optional string`

                A label for the address, e.g. WORK, HOME.

            - `org: optional object { company, department, title }`

              - `company: optional string`

              - `department: optional string`

              - `title: optional string`

            - `phones: optional array of object { phone, kind, messaging_id }`

              - `phone: string`

                The phone number, ideally in +E.164 form.

              - `kind: optional string`

                A label for the number, e.g. CELL, MAIN, WORK, HOME.

              - `messaging_id: optional string`

                The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

            - `urls: optional array of object { url, kind }`

              - `url: string`

              - `kind: optional string`

                A label for the URL, e.g. WORK, HOME.

          - `type: "contacts"`

            - `"contacts"`

        - `Location object { latitude, longitude, type, 2 more }`

          A location pin.

          - `latitude: number`

            Latitude in decimal degrees.

          - `longitude: number`

            Longitude in decimal degrees.

          - `type: "location"`

            - `"location"`

          - `address: optional string`

            The place's address, shown under the name.

          - `name: optional string`

            The place's name, shown on the pin.

        - `Reaction object { emoji, type, channel_message_id, message_id }`

          An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel's opaque channel_message_id; outbound reactions will carry this API's message_id once sending them becomes available.

          - `emoji: string`

            A single emoji. Send an empty string to remove a previous reaction to the same message.

          - `type: "reaction"`

            - `"reaction"`

          - `channel_message_id: optional string`

            Inbound only: the channel's opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

          - `message_id: optional string`

            Outbound only: this API's Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

        - `InteractiveReply object { id, kind, title, 2 more }`

          Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

          - `id: string`

            The id you assigned to the chosen row or button.

          - `kind: "list_reply" or "button_reply"`

            Which interactive message kind was answered.

            - `"list_reply"`

            - `"button_reply"`

          - `title: string`

            The chosen row's or button's visible title.

          - `type: "interactive_reply"`

            - `"interactive_reply"`

          - `description: optional string`

            The chosen list row's secondary text, when it had one.

        - `AddressReply object { type, values, saved_address_id }`

          Inbound only: the structured values submitted in response to an address request.

          - `type: "address_reply"`

            - `"address_reply"`

          - `values: object { address, building_name, city, 8 more }`

            - `address: optional string`

            - `building_name: optional string`

            - `city: optional string`

            - `floor_number: optional string`

            - `house_number: optional string`

            - `landmark_area: optional string`

            - `name: optional string`

            - `phone_number: optional string`

            - `pin_code: optional string`

            - `state: optional string`

            - `tower_number: optional string`

          - `saved_address_id: optional string`

            Identifier of the selected saved address, when one was selected.

        - `Order object { catalog_id, items, type, text }`

          Inbound only — never sendable. An order the customer placed from a product catalog.

          - `catalog_id: string`

            The catalog the ordered items belong to.

          - `items: array of object { currency, price, product_retailer_id, quantity }`

            The ordered items.

            - `currency: string`

              ISO 4217 currency code for price.

            - `price: number`

              The per-item price at order time.

            - `product_retailer_id: string`

              Your identifier for the product, as registered in the catalog.

            - `quantity: number`

          - `type: "order"`

            - `"order"`

          - `text: optional string`

            Free text the customer attached to the order, when any.

        - `Referral object { type, body, click_id, 7 more }`

          Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

          - `type: "referral"`

            - `"referral"`

          - `body: optional string`

            The ad's body text at click time.

          - `click_id: optional string`

            The click identifier assigned by the ad platform, for attribution.

          - `headline: optional string`

            The ad's headline at click time.

          - `image_url: optional string`

            URL of the ad's image creative, when media_kind is image.

          - `media_kind: optional string`

            The ad creative's media kind, e.g. image or video.

          - `source_id: optional string`

            The ad or post id.

          - `source_kind: optional string`

            What the source was, e.g. ad or post.

          - `source_url: optional string`

            The URL of the ad or post the customer came from.

          - `video_url: optional string`

            URL of the ad's video creative, when media_kind is video.

        - `System object { body, type, event }`

          Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

          - `body: string`

            Human-readable description of the system event.

          - `type: "system"`

            - `"system"`

          - `event: optional string`

            The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

        - `Unsupported object { raw, type, kind }`

          Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

          - `raw: unknown`

            The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

          - `type: "unsupported"`

            - `"unsupported"`

          - `kind: optional string`

            The channel's name for the message kind, when it declares one.

      - `status: string`

        The message's delivery state as last reported.

        Inbound messages read `received`: they arrived, and no delivery of ours
        ran. A message you sent starts `accepted` and moves through `sent`,
        `delivered` and `read` as the channel reports them, with `failed` as the
        definitive negative outcome.

        `unknown` means exactly that: the send is INDETERMINATE and we decline
        to guess. It is NOT terminal — an indeterminate send is never re-sent,
        and it resolves to `sent`, `delivered`, `read` or `failed` when the
        channel's own report arrives — so keep observing rather than treating it
        as an outcome.

        States are reported asynchronously on the chat's event sequence, and the
        set grows additively: treat a value you do not recognise as "no
        information" rather than failing on it.

        Retention of this published status follows the chat transcript policy:
        there is no scheduled pruning during beta, but there is no fixed minimum
        availability guarantee. The present absence of a scheduled age-based
        sweep is not a promise of indefinite availability.

        UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
        `sent` and then `delivered` within milliseconds, every time; `delivered`
        there means a simulator accepted it, not that a device received it. Live
        delivery can lag by hours, can never report `delivered` at all, and can
        fail after the channel accepted the message — so do not calibrate
        timeouts or "delivered means it arrived" logic against a test key.

      - `channel_message_id: optional string`

        The channel's opaque identifier for this message, when the channel has assigned one. Match an inbound reaction's `channel_message_id` to this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier.

      - `failure: optional object { code, message }`

        Why a failed message failed, when we have a reason worth publishing.

        Present only on `status` `failed`, and not on every one of those: not every
        channel refusal has a reason in the published vocabulary, so a message
        that failed on the wire may carry no `failure` at all. Its absence means
        "no published reason", never "no reason".

        `status` stays the thing to branch on. This is additive detail beside it,
        so an integration written before this field existed still sees a terminal
        `failed` and behaves exactly as it did.

        - `code: string`

          The specific reason.

          `sender_deregistered` — the number this message was going out from is no
          longer a registered sending number, and the message never left. This is
          terminal: it is not a pause and it does not clear, so retrying the same
          message cannot succeed and that conversation is over. Reach this customer
          by starting a new one on another of your numbers with a template, exactly
          as you would any customer whose 24-hour window has closed; `GET /v1/chats`
          shows which number each conversation uses. You are not billed for a
          message that failed this way.

          `quality_hold_expired` — this marketing message remained held by an
          automatic safety control on its sending line for ten minutes and was not
          sent. This is terminal: retrying the same message cannot succeed on that
          conversation. Send marketing from another of your numbers with a template,
          or contact support about the sending line. You are not billed for a message
          that failed this way.

        - `message: string`

          Human-readable, written for a person reading it. Do not branch on it.

      - `from: optional string`

        The number this message left by, or arrived on, in E.164 (leading `+`).
        Every message in one conversation carries the same value — a
        conversation never moves to another number. Absent only if the number is
        no longer one of yours.

  - `has_more: boolean`

    True when another page exists — pass `next_cursor` to fetch it.

  - `next_cursor: optional string`

    Opaque cursor for the next page; present when `has_more` is true.

### Chat Page

- `ChatPage object { data, has_more, next_cursor }`

  A page of chats.

  - `data: array of Chat`

    - `id: string`

      The chat's id.

    - `created_at: string`

    - `customer: string`

      Customer address as an opaque identifier.

    - `customer_window: object { expires_at, state, source }`

      The customer service window — first-class state on every chat read. Only
      `template` parts are sendable while it is closed.

      - `expires_at: string`

        When the open window expires. `null` when no window has ever opened.

      - `state: string`

        `open` or `closed`.

      - `source: optional string`

        What opened the current window: `customer_message`, or `free_entry_point` for ad-sourced windows. Absent when no window has ever opened.

    - `phone_number_id: string`

      The id of the sending number this chat is pinned to.

    - `updated_at: string`

      When the chat last changed — a new event moves it.

    - `from: optional string`

      The number this conversation sends from, in E.164 (leading `+`). This is
      the value to pass back as a send's `from` to address this exact
      conversation. Absent only if the number is no longer one of yours.

  - `has_more: boolean`

    True when another page exists — pass `next_cursor` to fetch it.

  - `next_cursor: optional string`

    Opaque cursor for the next page; present when `has_more` is true.

# Events

## List a chat's events

**get** `/v1/chats/{chat}/events`

Reads a chat's event sequence in order — the transcript API. The
sequence is gapless per chat: page forward from `seq` 1 (or any cursor)
and nothing is ever missing between two entries.

This is the BACKFILL for `GET /v1/streams/events`, which is a live stream and
replays nothing: an event that passed while you were not connected is
re-read here, addressed by the `chat_id` and `seq` range every stream
frame carries.

The cursor is the `seq` of the last entry you processed, and it is
exclusive — pass `next_cursor` back verbatim, or the `seq` you stopped
at, and the next page starts after it. Paging never skips: the next page
resumes from the last position actually handed to you, so an event
journaled between two of your requests is returned rather than stepped
over.

**Being caught up and being lost are different answers.** A cursor at
the end of the sequence returns an empty page — that is the ordinary
polling result. A cursor BEYOND the end is refused with `422`
(`invalid_paging`), because no such position has ever existed in this
chat and answering it with silence would be indistinguishable from
having caught up. The refusal names where the sequence actually ends, so
a client that has drifted can resume. An empty cursor is refused the
same way: omit the parameter to read from the beginning.

**Retention.** This journal has no scheduled pruning during beta, so
ordinary operation does not remove an old prefix or renumber its
immutable sequence. That is not a permanent-retention promise or a
fixed minimum availability period: authenticated account deletion,
offboarding, a lawful deletion requirement, or a beta-environment reset
may remove the account-scoped transcript. This operation therefore has
no `expires_at`, retained-floor field, or scheduled `cursor_expired`
response. If prefix pruning is introduced later, its additive retained
floor and typed expiry behavior will be published before any history is
removed.

### Path Parameters

- `chat: string`

### Query Parameters

- `cursor: optional string`

  Cursor from a previous page's `next_cursor`; pages run in ascending `seq` order.

- `limit: optional number`

  Page size. The bounds are in this parameter's schema, not only in this sentence — a value outside them is refused, never clamped.

### Returns

- `ChatEventPage object { data, has_more, next_cursor }`

  A page of one chat's events, in sequence order.

  - `data: array of ChatEvent`

    - `occurred_at: string`

    - `seq: number`

      The event's position in this chat's sequence — contiguous from 1, no gaps. Use it as this chat's events-read cursor. It is scoped to one chat, so it is NOT the `/v1/streams/events` stream cursor: that stream spans every chat and carries its own cursor in each frame's `id`.

    - `type: string`

      The event's kind, e.g. `message.received`, `message.sent`. Kinds grow
      additively — skip what you do not handle.

      `chat.event.unknown` is the one kind that never carries meaning: it
      marks a journal entry whose partner-visible kind is not published yet.
      It exists so the sequence stays gapless — an entry you cannot interpret
      is still an entry, and omitting it would put a hole in `seq`. Skip it.

    - `message: optional Message`

      The message this event is about, on message-kind events.

      - `id: string`

        This API's message id. Once outbound reactions are available, put this value in their `message_id`. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

      - `chat_id: string`

        The chat this message belongs to.

      - `created_at: string`

      - `direction: "inbound" or "outbound"`

        `inbound` (from the customer) or `outbound` (sent by you).

        - `"inbound"`

        - `"outbound"`

      - `parts: array of object { body, type, preview_url }  or object { kind, type, caption, 4 more }  or object { language, name, type, 3 more }  or 14 more`

        The message content — sendable parts, plus the inbound-only types on
        inbound messages. Every message carries at least one part.

        - `Text object { body, type, preview_url }`

          A plain text message.

          - `body: string`

            The message text. URLs render as tappable links.

          - `type: "text"`

            - `"text"`

          - `preview_url: optional boolean`

            Render a preview card for the first URL in body. Defaults to false.

        - `Media object { kind, type, caption, 4 more }`

          A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

          - `kind: "image" or "video" or "audio" or 2 more`

            Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

            - `"image"`

            - `"video"`

            - `"audio"`

            - `"document"`

            - `"sticker"`

          - `type: "media"`

            - `"media"`

          - `caption: optional string`

            Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

          - `filename: optional string`

            Display filename. Documents only — a filename on any other kind is rejected, never dropped.

          - `media_id: optional string`

            Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

          - `upload_ref: optional string`

            Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

          - `url: optional string`

            Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

        - `Template object { language, name, type, 3 more }`

          An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template's published parameter schema explicitly names one. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

          - `language: string`

            The template's language-and-locale code, e.g. en_US.

          - `name: string`

            The approved template's name. Lowercase letters, digits and underscores only.

          - `type: "template"`

            - `"template"`

          - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

            Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

            - `card_index: number`

            - `kind: "image" or "video"`

              - `"image"`

              - `"video"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

            - `buttons: optional array of object { index, payload, type }`

              - `index: number`

              - `payload: string`

              - `type: "quick_reply" or "url"`

                - `"quick_reply"`

                - `"url"`

            - `parameters: optional unknown`

              Named values for this card's body placeholders.

          - `header: optional object { kind, upload_ref }`

            The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

            - `kind: "image" or "video" or "document"`

              - `"image"`

              - `"video"`

              - `"document"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media.

          - `parameters: optional unknown`

            Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

        - `InteractiveList object { body, button, sections, 3 more }`

          An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

          - `body: string`

            The message body.

          - `button: string`

            Label of the button that opens the list.

          - `sections: array of object { rows, title }`

            The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

            - `rows: array of object { id, title, description }`

              This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

              - `id: string`

                Your identifier for the row — echoed back as the interactive_reply's id.

              - `title: string`

                The row's visible title.

              - `description: optional string`

                Secondary text under the title.

            - `title: optional string`

              Section title. Required when the list has more than one section.

          - `type: "interactive_list"`

            - `"interactive_list"`

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

        - `InteractiveButtons object { body, buttons, type, 3 more }`

          An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

          - `body: string`

            The message body.

          - `buttons: array of object { id, title }`

            The tappable buttons — at most three.

            - `id: string`

              Your identifier for the button — echoed back as the interactive_reply's id.

            - `title: string`

              The button's visible label.

          - `type: "interactive_buttons"`

            - `"interactive_buttons"`

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

          - `media_header: optional object { kind, upload_ref }`

            Uploaded image, video, or document above the body. Use either header or media_header, never both.

            - `kind: "image" or "video" or "document"`

              - `"image"`

              - `"video"`

              - `"document"`

            - `upload_ref: string`

              A durable handle returned by POST /v1/media.

        - `InteractiveCtaURL object { body, display_text, type, 3 more }`

          An interactive call-to-action message: body text plus one button that opens a URL.

          - `body: string`

            The message body.

          - `display_text: string`

            The button's visible label.

          - `type: "interactive_cta_url"`

            - `"interactive_cta_url"`

          - `url: string`

            The URL the button opens. Must be absolute, with an http or https scheme.

          - `footer: optional string`

            Footer text below the body.

          - `header: optional string`

            Header text above the body.

        - `LocationRequest object { body, type }`

          Asks the customer to share a location. The response arrives as a location part.

          - `body: string`

            Prompt shown above the send-location button.

          - `type: "location_request"`

            - `"location_request"`

        - `AddressRequest object { body, country, type }`

          Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

          - `body: string`

            Prompt shown above the address form.

          - `country: "IN"`

            ISO country code. Structured address requests are currently available only in India.

            - `"IN"`

          - `type: "address_request"`

            - `"address_request"`

        - `Contacts object { contacts, type }`

          One or more contact cards.

          - `contacts: array of object { name, addresses, birthday, 4 more }`

            The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

            - `name: object { formatted_name, first_name, last_name, 3 more }`

              - `formatted_name: string`

                The contact's full display name.

              - `first_name: optional string`

              - `last_name: optional string`

              - `middle_name: optional string`

              - `prefix: optional string`

              - `suffix: optional string`

            - `addresses: optional array of object { city, country, country_code, 4 more }`

              - `city: optional string`

              - `country: optional string`

              - `country_code: optional string`

              - `kind: optional string`

                A label for the address, e.g. WORK, HOME.

              - `state: optional string`

              - `street: optional string`

              - `zip: optional string`

            - `birthday: optional string`

              The contact's birthday as YYYY-MM-DD.

            - `emails: optional array of object { email, kind }`

              - `email: string`

              - `kind: optional string`

                A label for the address, e.g. WORK, HOME.

            - `org: optional object { company, department, title }`

              - `company: optional string`

              - `department: optional string`

              - `title: optional string`

            - `phones: optional array of object { phone, kind, messaging_id }`

              - `phone: string`

                The phone number, ideally in +E.164 form.

              - `kind: optional string`

                A label for the number, e.g. CELL, MAIN, WORK, HOME.

              - `messaging_id: optional string`

                The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

            - `urls: optional array of object { url, kind }`

              - `url: string`

              - `kind: optional string`

                A label for the URL, e.g. WORK, HOME.

          - `type: "contacts"`

            - `"contacts"`

        - `Location object { latitude, longitude, type, 2 more }`

          A location pin.

          - `latitude: number`

            Latitude in decimal degrees.

          - `longitude: number`

            Longitude in decimal degrees.

          - `type: "location"`

            - `"location"`

          - `address: optional string`

            The place's address, shown under the name.

          - `name: optional string`

            The place's name, shown on the pin.

        - `Reaction object { emoji, type, channel_message_id, message_id }`

          An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel's opaque channel_message_id; outbound reactions will carry this API's message_id once sending them becomes available.

          - `emoji: string`

            A single emoji. Send an empty string to remove a previous reaction to the same message.

          - `type: "reaction"`

            - `"reaction"`

          - `channel_message_id: optional string`

            Inbound only: the channel's opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

          - `message_id: optional string`

            Outbound only: this API's Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

        - `InteractiveReply object { id, kind, title, 2 more }`

          Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

          - `id: string`

            The id you assigned to the chosen row or button.

          - `kind: "list_reply" or "button_reply"`

            Which interactive message kind was answered.

            - `"list_reply"`

            - `"button_reply"`

          - `title: string`

            The chosen row's or button's visible title.

          - `type: "interactive_reply"`

            - `"interactive_reply"`

          - `description: optional string`

            The chosen list row's secondary text, when it had one.

        - `AddressReply object { type, values, saved_address_id }`

          Inbound only: the structured values submitted in response to an address request.

          - `type: "address_reply"`

            - `"address_reply"`

          - `values: object { address, building_name, city, 8 more }`

            - `address: optional string`

            - `building_name: optional string`

            - `city: optional string`

            - `floor_number: optional string`

            - `house_number: optional string`

            - `landmark_area: optional string`

            - `name: optional string`

            - `phone_number: optional string`

            - `pin_code: optional string`

            - `state: optional string`

            - `tower_number: optional string`

          - `saved_address_id: optional string`

            Identifier of the selected saved address, when one was selected.

        - `Order object { catalog_id, items, type, text }`

          Inbound only — never sendable. An order the customer placed from a product catalog.

          - `catalog_id: string`

            The catalog the ordered items belong to.

          - `items: array of object { currency, price, product_retailer_id, quantity }`

            The ordered items.

            - `currency: string`

              ISO 4217 currency code for price.

            - `price: number`

              The per-item price at order time.

            - `product_retailer_id: string`

              Your identifier for the product, as registered in the catalog.

            - `quantity: number`

          - `type: "order"`

            - `"order"`

          - `text: optional string`

            Free text the customer attached to the order, when any.

        - `Referral object { type, body, click_id, 7 more }`

          Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

          - `type: "referral"`

            - `"referral"`

          - `body: optional string`

            The ad's body text at click time.

          - `click_id: optional string`

            The click identifier assigned by the ad platform, for attribution.

          - `headline: optional string`

            The ad's headline at click time.

          - `image_url: optional string`

            URL of the ad's image creative, when media_kind is image.

          - `media_kind: optional string`

            The ad creative's media kind, e.g. image or video.

          - `source_id: optional string`

            The ad or post id.

          - `source_kind: optional string`

            What the source was, e.g. ad or post.

          - `source_url: optional string`

            The URL of the ad or post the customer came from.

          - `video_url: optional string`

            URL of the ad's video creative, when media_kind is video.

        - `System object { body, type, event }`

          Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

          - `body: string`

            Human-readable description of the system event.

          - `type: "system"`

            - `"system"`

          - `event: optional string`

            The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

        - `Unsupported object { raw, type, kind }`

          Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

          - `raw: unknown`

            The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

          - `type: "unsupported"`

            - `"unsupported"`

          - `kind: optional string`

            The channel's name for the message kind, when it declares one.

      - `status: string`

        The message's delivery state as last reported.

        Inbound messages read `received`: they arrived, and no delivery of ours
        ran. A message you sent starts `accepted` and moves through `sent`,
        `delivered` and `read` as the channel reports them, with `failed` as the
        definitive negative outcome.

        `unknown` means exactly that: the send is INDETERMINATE and we decline
        to guess. It is NOT terminal — an indeterminate send is never re-sent,
        and it resolves to `sent`, `delivered`, `read` or `failed` when the
        channel's own report arrives — so keep observing rather than treating it
        as an outcome.

        States are reported asynchronously on the chat's event sequence, and the
        set grows additively: treat a value you do not recognise as "no
        information" rather than failing on it.

        Retention of this published status follows the chat transcript policy:
        there is no scheduled pruning during beta, but there is no fixed minimum
        availability guarantee. The present absence of a scheduled age-based
        sweep is not a promise of indefinite availability.

        UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
        `sent` and then `delivered` within milliseconds, every time; `delivered`
        there means a simulator accepted it, not that a device received it. Live
        delivery can lag by hours, can never report `delivered` at all, and can
        fail after the channel accepted the message — so do not calibrate
        timeouts or "delivered means it arrived" logic against a test key.

      - `channel_message_id: optional string`

        The channel's opaque identifier for this message, when the channel has assigned one. Match an inbound reaction's `channel_message_id` to this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier.

      - `failure: optional object { code, message }`

        Why a failed message failed, when we have a reason worth publishing.

        Present only on `status` `failed`, and not on every one of those: not every
        channel refusal has a reason in the published vocabulary, so a message
        that failed on the wire may carry no `failure` at all. Its absence means
        "no published reason", never "no reason".

        `status` stays the thing to branch on. This is additive detail beside it,
        so an integration written before this field existed still sees a terminal
        `failed` and behaves exactly as it did.

        - `code: string`

          The specific reason.

          `sender_deregistered` — the number this message was going out from is no
          longer a registered sending number, and the message never left. This is
          terminal: it is not a pause and it does not clear, so retrying the same
          message cannot succeed and that conversation is over. Reach this customer
          by starting a new one on another of your numbers with a template, exactly
          as you would any customer whose 24-hour window has closed; `GET /v1/chats`
          shows which number each conversation uses. You are not billed for a
          message that failed this way.

          `quality_hold_expired` — this marketing message remained held by an
          automatic safety control on its sending line for ten minutes and was not
          sent. This is terminal: retrying the same message cannot succeed on that
          conversation. Send marketing from another of your numbers with a template,
          or contact support about the sending line. You are not billed for a message
          that failed this way.

        - `message: string`

          Human-readable, written for a person reading it. Do not branch on it.

      - `from: optional string`

        The number this message left by, or arrived on, in E.164 (leading `+`).
        Every message in one conversation carries the same value — a
        conversation never moves to another number. Absent only if the number is
        no longer one of yours.

  - `has_more: boolean`

    True when another page exists — pass `next_cursor` to fetch it.

  - `next_cursor: optional string`

    Opaque cursor for the next page; present when `has_more` is true.

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/chats/$CHAT/events \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY"
```

#### Response

```json
{
  "data": [
    {
      "occurred_at": "2019-12-27T18:11:19.117Z",
      "seq": 0,
      "type": "type",
      "message": {
        "id": "id",
        "chat_id": "chat_id",
        "created_at": "2019-12-27T18:11:19.117Z",
        "direction": "inbound",
        "parts": [
          {
            "body": "body",
            "type": "text",
            "preview_url": true
          }
        ],
        "status": "status",
        "channel_message_id": "channel_message_id",
        "failure": {
          "code": "code",
          "message": "message"
        },
        "from": "from"
      }
    }
  ],
  "has_more": true,
  "next_cursor": "next_cursor"
}
```

# Messages

## Send a message in a chat

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

Sends a reply on an existing chat's unchanged sending number. The chat
supplies the customer and sender; this operation never creates, moves,
repins or fails over a conversation. Acceptance and refusal semantics are
otherwise identical to `POST /v1/messages`.

### Path Parameters

- `chat: string`

### Header Parameters

- `"Idempotency-Key": optional string`

### Body Parameters

- `parts: array of object { body, type, preview_url }  or object { kind, type, caption, 4 more }  or object { language, name, type, 3 more }  or 8 more`

  The message content — sendable parts only, at least one.

  **One outbound part per request.** Each request has its own acceptance
  result and optional idempotency key. Each accepted request maps to one
  native channel message with its own identifier, delivery outcome and
  charge evidence. A request carrying more than one
  part is refused with HTTP 422 (`multiple_parts_not_available`); the API
  neither fans it out nor offers an atomic batch. Separate requests are
  independent, and no cross-request delivery or receipt order is promised.

  - `Text object { body, type, preview_url }`

    A plain text message.

    - `body: string`

      The message text. URLs render as tappable links.

    - `type: "text"`

      - `"text"`

    - `preview_url: optional boolean`

      Render a preview card for the first URL in body. Defaults to false.

  - `Media object { kind, type, caption, 4 more }`

    A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

    - `kind: "image" or "video" or "audio" or 2 more`

      Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

      - `"image"`

      - `"video"`

      - `"audio"`

      - `"document"`

      - `"sticker"`

    - `type: "media"`

      - `"media"`

    - `caption: optional string`

      Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

    - `filename: optional string`

      Display filename. Documents only — a filename on any other kind is rejected, never dropped.

    - `media_id: optional string`

      Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

    - `upload_ref: optional string`

      Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

    - `url: optional string`

      Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

  - `Template object { language, name, type, 3 more }`

    An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template's published parameter schema explicitly names one. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

    - `language: string`

      The template's language-and-locale code, e.g. en_US.

    - `name: string`

      The approved template's name. Lowercase letters, digits and underscores only.

    - `type: "template"`

      - `"template"`

    - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

      Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

      - `card_index: number`

      - `kind: "image" or "video"`

        - `"image"`

        - `"video"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

      - `buttons: optional array of object { index, payload, type }`

        - `index: number`

        - `payload: string`

        - `type: "quick_reply" or "url"`

          - `"quick_reply"`

          - `"url"`

      - `parameters: optional unknown`

        Named values for this card's body placeholders.

    - `header: optional object { kind, upload_ref }`

      The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

      - `kind: "image" or "video" or "document"`

        - `"image"`

        - `"video"`

        - `"document"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media.

    - `parameters: optional unknown`

      Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

  - `InteractiveList object { body, button, sections, 3 more }`

    An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

    - `body: string`

      The message body.

    - `button: string`

      Label of the button that opens the list.

    - `sections: array of object { rows, title }`

      The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

      - `rows: array of object { id, title, description }`

        This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

        - `id: string`

          Your identifier for the row — echoed back as the interactive_reply's id.

        - `title: string`

          The row's visible title.

        - `description: optional string`

          Secondary text under the title.

      - `title: optional string`

        Section title. Required when the list has more than one section.

    - `type: "interactive_list"`

      - `"interactive_list"`

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

  - `InteractiveButtons object { body, buttons, type, 3 more }`

    An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

    - `body: string`

      The message body.

    - `buttons: array of object { id, title }`

      The tappable buttons — at most three.

      - `id: string`

        Your identifier for the button — echoed back as the interactive_reply's id.

      - `title: string`

        The button's visible label.

    - `type: "interactive_buttons"`

      - `"interactive_buttons"`

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

    - `media_header: optional object { kind, upload_ref }`

      Uploaded image, video, or document above the body. Use either header or media_header, never both.

      - `kind: "image" or "video" or "document"`

        - `"image"`

        - `"video"`

        - `"document"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media.

  - `InteractiveCtaURL object { body, display_text, type, 3 more }`

    An interactive call-to-action message: body text plus one button that opens a URL.

    - `body: string`

      The message body.

    - `display_text: string`

      The button's visible label.

    - `type: "interactive_cta_url"`

      - `"interactive_cta_url"`

    - `url: string`

      The URL the button opens. Must be absolute, with an http or https scheme.

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

  - `LocationRequest object { body, type }`

    Asks the customer to share a location. The response arrives as a location part.

    - `body: string`

      Prompt shown above the send-location button.

    - `type: "location_request"`

      - `"location_request"`

  - `AddressRequest object { body, country, type }`

    Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

    - `body: string`

      Prompt shown above the address form.

    - `country: "IN"`

      ISO country code. Structured address requests are currently available only in India.

      - `"IN"`

    - `type: "address_request"`

      - `"address_request"`

  - `Contacts object { contacts, type }`

    One or more contact cards.

    - `contacts: array of object { name, addresses, birthday, 4 more }`

      The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

      - `name: object { formatted_name, first_name, last_name, 3 more }`

        - `formatted_name: string`

          The contact's full display name.

        - `first_name: optional string`

        - `last_name: optional string`

        - `middle_name: optional string`

        - `prefix: optional string`

        - `suffix: optional string`

      - `addresses: optional array of object { city, country, country_code, 4 more }`

        - `city: optional string`

        - `country: optional string`

        - `country_code: optional string`

        - `kind: optional string`

          A label for the address, e.g. WORK, HOME.

        - `state: optional string`

        - `street: optional string`

        - `zip: optional string`

      - `birthday: optional string`

        The contact's birthday as YYYY-MM-DD.

      - `emails: optional array of object { email, kind }`

        - `email: string`

        - `kind: optional string`

          A label for the address, e.g. WORK, HOME.

      - `org: optional object { company, department, title }`

        - `company: optional string`

        - `department: optional string`

        - `title: optional string`

      - `phones: optional array of object { phone, kind, messaging_id }`

        - `phone: string`

          The phone number, ideally in +E.164 form.

        - `kind: optional string`

          A label for the number, e.g. CELL, MAIN, WORK, HOME.

        - `messaging_id: optional string`

          The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

      - `urls: optional array of object { url, kind }`

        - `url: string`

        - `kind: optional string`

          A label for the URL, e.g. WORK, HOME.

    - `type: "contacts"`

      - `"contacts"`

  - `Location object { latitude, longitude, type, 2 more }`

    A location pin.

    - `latitude: number`

      Latitude in decimal degrees.

    - `longitude: number`

      Longitude in decimal degrees.

    - `type: "location"`

      - `"location"`

    - `address: optional string`

      The place's address, shown under the name.

    - `name: optional string`

      The place's name, shown on the pin.

  - `Reaction object { emoji, type, channel_message_id, message_id }`

    An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel's opaque channel_message_id; outbound reactions will carry this API's message_id once sending them becomes available.

    - `emoji: string`

      A single emoji. Send an empty string to remove a previous reaction to the same message.

    - `type: "reaction"`

      - `"reaction"`

    - `channel_message_id: optional string`

      Inbound only: the channel's opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

    - `message_id: optional string`

      Outbound only: this API's Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

- `reply_to_message_id: optional string`

  Our opaque `Message.id` for an earlier message in this same chat.

- `send_at: optional string`

  RESERVED — scheduled sends are not yet available. Any value is refused.

### Returns

- `AcceptedMessage object { id, chat_id, created_at, 7 more }`

  An accepted send: the message, plus how its sending number was chosen.

  - `id: string`

    This API's message id. Once outbound reactions are available, put this value in their `message_id`. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

  - `chat_id: string`

    The chat this message belongs to.

  - `created_at: string`

  - `direction: "inbound" or "outbound"`

    `inbound` (from the customer) or `outbound` (sent by you).

    - `"inbound"`

    - `"outbound"`

  - `parts: array of object { body, type, preview_url }  or object { kind, type, caption, 4 more }  or object { language, name, type, 3 more }  or 14 more`

    The message content — sendable parts, plus the inbound-only types on
    inbound messages. Every message carries at least one part.

    - `Text object { body, type, preview_url }`

      A plain text message.

      - `body: string`

        The message text. URLs render as tappable links.

      - `type: "text"`

        - `"text"`

      - `preview_url: optional boolean`

        Render a preview card for the first URL in body. Defaults to false.

    - `Media object { kind, type, caption, 4 more }`

      A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

      - `kind: "image" or "video" or "audio" or 2 more`

        Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

        - `"image"`

        - `"video"`

        - `"audio"`

        - `"document"`

        - `"sticker"`

      - `type: "media"`

        - `"media"`

      - `caption: optional string`

        Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

      - `filename: optional string`

        Display filename. Documents only — a filename on any other kind is rejected, never dropped.

      - `media_id: optional string`

        Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

      - `upload_ref: optional string`

        Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

      - `url: optional string`

        Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

    - `Template object { language, name, type, 3 more }`

      An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template's published parameter schema explicitly names one. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

      - `language: string`

        The template's language-and-locale code, e.g. en_US.

      - `name: string`

        The approved template's name. Lowercase letters, digits and underscores only.

      - `type: "template"`

        - `"template"`

      - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

        Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

        - `card_index: number`

        - `kind: "image" or "video"`

          - `"image"`

          - `"video"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

        - `buttons: optional array of object { index, payload, type }`

          - `index: number`

          - `payload: string`

          - `type: "quick_reply" or "url"`

            - `"quick_reply"`

            - `"url"`

        - `parameters: optional unknown`

          Named values for this card's body placeholders.

      - `header: optional object { kind, upload_ref }`

        The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

      - `parameters: optional unknown`

        Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

    - `InteractiveList object { body, button, sections, 3 more }`

      An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

      - `body: string`

        The message body.

      - `button: string`

        Label of the button that opens the list.

      - `sections: array of object { rows, title }`

        The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

        - `rows: array of object { id, title, description }`

          This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

          - `id: string`

            Your identifier for the row — echoed back as the interactive_reply's id.

          - `title: string`

            The row's visible title.

          - `description: optional string`

            Secondary text under the title.

        - `title: optional string`

          Section title. Required when the list has more than one section.

      - `type: "interactive_list"`

        - `"interactive_list"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

    - `InteractiveButtons object { body, buttons, type, 3 more }`

      An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

      - `body: string`

        The message body.

      - `buttons: array of object { id, title }`

        The tappable buttons — at most three.

        - `id: string`

          Your identifier for the button — echoed back as the interactive_reply's id.

        - `title: string`

          The button's visible label.

      - `type: "interactive_buttons"`

        - `"interactive_buttons"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

    - `InteractiveCtaURL object { body, display_text, type, 3 more }`

      An interactive call-to-action message: body text plus one button that opens a URL.

      - `body: string`

        The message body.

      - `display_text: string`

        The button's visible label.

      - `type: "interactive_cta_url"`

        - `"interactive_cta_url"`

      - `url: string`

        The URL the button opens. Must be absolute, with an http or https scheme.

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

    - `LocationRequest object { body, type }`

      Asks the customer to share a location. The response arrives as a location part.

      - `body: string`

        Prompt shown above the send-location button.

      - `type: "location_request"`

        - `"location_request"`

    - `AddressRequest object { body, country, type }`

      Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

      - `body: string`

        Prompt shown above the address form.

      - `country: "IN"`

        ISO country code. Structured address requests are currently available only in India.

        - `"IN"`

      - `type: "address_request"`

        - `"address_request"`

    - `Contacts object { contacts, type }`

      One or more contact cards.

      - `contacts: array of object { name, addresses, birthday, 4 more }`

        The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

        - `name: object { formatted_name, first_name, last_name, 3 more }`

          - `formatted_name: string`

            The contact's full display name.

          - `first_name: optional string`

          - `last_name: optional string`

          - `middle_name: optional string`

          - `prefix: optional string`

          - `suffix: optional string`

        - `addresses: optional array of object { city, country, country_code, 4 more }`

          - `city: optional string`

          - `country: optional string`

          - `country_code: optional string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

          - `state: optional string`

          - `street: optional string`

          - `zip: optional string`

        - `birthday: optional string`

          The contact's birthday as YYYY-MM-DD.

        - `emails: optional array of object { email, kind }`

          - `email: string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

        - `org: optional object { company, department, title }`

          - `company: optional string`

          - `department: optional string`

          - `title: optional string`

        - `phones: optional array of object { phone, kind, messaging_id }`

          - `phone: string`

            The phone number, ideally in +E.164 form.

          - `kind: optional string`

            A label for the number, e.g. CELL, MAIN, WORK, HOME.

          - `messaging_id: optional string`

            The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

        - `urls: optional array of object { url, kind }`

          - `url: string`

          - `kind: optional string`

            A label for the URL, e.g. WORK, HOME.

      - `type: "contacts"`

        - `"contacts"`

    - `Location object { latitude, longitude, type, 2 more }`

      A location pin.

      - `latitude: number`

        Latitude in decimal degrees.

      - `longitude: number`

        Longitude in decimal degrees.

      - `type: "location"`

        - `"location"`

      - `address: optional string`

        The place's address, shown under the name.

      - `name: optional string`

        The place's name, shown on the pin.

    - `Reaction object { emoji, type, channel_message_id, message_id }`

      An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel's opaque channel_message_id; outbound reactions will carry this API's message_id once sending them becomes available.

      - `emoji: string`

        A single emoji. Send an empty string to remove a previous reaction to the same message.

      - `type: "reaction"`

        - `"reaction"`

      - `channel_message_id: optional string`

        Inbound only: the channel's opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

      - `message_id: optional string`

        Outbound only: this API's Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

    - `InteractiveReply object { id, kind, title, 2 more }`

      Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

      - `id: string`

        The id you assigned to the chosen row or button.

      - `kind: "list_reply" or "button_reply"`

        Which interactive message kind was answered.

        - `"list_reply"`

        - `"button_reply"`

      - `title: string`

        The chosen row's or button's visible title.

      - `type: "interactive_reply"`

        - `"interactive_reply"`

      - `description: optional string`

        The chosen list row's secondary text, when it had one.

    - `AddressReply object { type, values, saved_address_id }`

      Inbound only: the structured values submitted in response to an address request.

      - `type: "address_reply"`

        - `"address_reply"`

      - `values: object { address, building_name, city, 8 more }`

        - `address: optional string`

        - `building_name: optional string`

        - `city: optional string`

        - `floor_number: optional string`

        - `house_number: optional string`

        - `landmark_area: optional string`

        - `name: optional string`

        - `phone_number: optional string`

        - `pin_code: optional string`

        - `state: optional string`

        - `tower_number: optional string`

      - `saved_address_id: optional string`

        Identifier of the selected saved address, when one was selected.

    - `Order object { catalog_id, items, type, text }`

      Inbound only — never sendable. An order the customer placed from a product catalog.

      - `catalog_id: string`

        The catalog the ordered items belong to.

      - `items: array of object { currency, price, product_retailer_id, quantity }`

        The ordered items.

        - `currency: string`

          ISO 4217 currency code for price.

        - `price: number`

          The per-item price at order time.

        - `product_retailer_id: string`

          Your identifier for the product, as registered in the catalog.

        - `quantity: number`

      - `type: "order"`

        - `"order"`

      - `text: optional string`

        Free text the customer attached to the order, when any.

    - `Referral object { type, body, click_id, 7 more }`

      Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

      - `type: "referral"`

        - `"referral"`

      - `body: optional string`

        The ad's body text at click time.

      - `click_id: optional string`

        The click identifier assigned by the ad platform, for attribution.

      - `headline: optional string`

        The ad's headline at click time.

      - `image_url: optional string`

        URL of the ad's image creative, when media_kind is image.

      - `media_kind: optional string`

        The ad creative's media kind, e.g. image or video.

      - `source_id: optional string`

        The ad or post id.

      - `source_kind: optional string`

        What the source was, e.g. ad or post.

      - `source_url: optional string`

        The URL of the ad or post the customer came from.

      - `video_url: optional string`

        URL of the ad's video creative, when media_kind is video.

    - `System object { body, type, event }`

      Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

      - `body: string`

        Human-readable description of the system event.

      - `type: "system"`

        - `"system"`

      - `event: optional string`

        The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

    - `Unsupported object { raw, type, kind }`

      Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

      - `raw: unknown`

        The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

      - `type: "unsupported"`

        - `"unsupported"`

      - `kind: optional string`

        The channel's name for the message kind, when it declares one.

  - `status: string`

    The message's delivery state as last reported.

    Inbound messages read `received`: they arrived, and no delivery of ours
    ran. A message you sent starts `accepted` and moves through `sent`,
    `delivered` and `read` as the channel reports them, with `failed` as the
    definitive negative outcome.

    `unknown` means exactly that: the send is INDETERMINATE and we decline
    to guess. It is NOT terminal — an indeterminate send is never re-sent,
    and it resolves to `sent`, `delivered`, `read` or `failed` when the
    channel's own report arrives — so keep observing rather than treating it
    as an outcome.

    States are reported asynchronously on the chat's event sequence, and the
    set grows additively: treat a value you do not recognise as "no
    information" rather than failing on it.

    Retention of this published status follows the chat transcript policy:
    there is no scheduled pruning during beta, but there is no fixed minimum
    availability guarantee. The present absence of a scheduled age-based
    sweep is not a promise of indefinite availability.

    UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
    `sent` and then `delivered` within milliseconds, every time; `delivered`
    there means a simulator accepted it, not that a device received it. Live
    delivery can lag by hours, can never report `delivered` at all, and can
    fail after the channel accepted the message — so do not calibrate
    timeouts or "delivered means it arrived" logic against a test key.

  - `channel_message_id: optional string`

    The channel's opaque identifier for this message, when the channel has assigned one. Match an inbound reaction's `channel_message_id` to this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier.

  - `failure: optional object { code, message }`

    Why a failed message failed, when we have a reason worth publishing.

    Present only on `status` `failed`, and not on every one of those: not every
    channel refusal has a reason in the published vocabulary, so a message
    that failed on the wire may carry no `failure` at all. Its absence means
    "no published reason", never "no reason".

    `status` stays the thing to branch on. This is additive detail beside it,
    so an integration written before this field existed still sees a terminal
    `failed` and behaves exactly as it did.

    - `code: string`

      The specific reason.

      `sender_deregistered` — the number this message was going out from is no
      longer a registered sending number, and the message never left. This is
      terminal: it is not a pause and it does not clear, so retrying the same
      message cannot succeed and that conversation is over. Reach this customer
      by starting a new one on another of your numbers with a template, exactly
      as you would any customer whose 24-hour window has closed; `GET /v1/chats`
      shows which number each conversation uses. You are not billed for a
      message that failed this way.

      `quality_hold_expired` — this marketing message remained held by an
      automatic safety control on its sending line for ten minutes and was not
      sent. This is terminal: retrying the same message cannot succeed on that
      conversation. Send marketing from another of your numbers with a template,
      or contact support about the sending line. You are not billed for a message
      that failed this way.

    - `message: string`

      Human-readable, written for a person reading it. Do not branch on it.

  - `from: optional string`

    The number this message left by, or arrived on, in E.164 (leading `+`).
    Every message in one conversation carries the same value — a
    conversation never moves to another number. Absent only if the number is
    no longer one of yours.

  - `from_selection: optional object { reason }`

    Present only when this service chose the sending number for you — that
    is, when you omitted `from`. A send that named `from` omits this rather
    than reporting a choice nobody made.

    - `reason: string`

      Why this number was used. Exactly one of:

      - `reused_active_chat` — this customer already had a conversation and it
        continues on the same number.
      - `new_best_number` — there was no existing conversation, so the best
        available number was chosen.

      Like every other vocabulary here it is a plain string that may grow
      additively; treat a value you do not recognise as "chosen for you".

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/chats/$CHAT/messages \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY" \
    -d '{
          "parts": [
            {
              "body": "body",
              "type": "text"
            }
          ]
        }'
```

#### Response

```json
{
  "id": "id",
  "chat_id": "chat_id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "direction": "inbound",
  "parts": [
    {
      "body": "body",
      "type": "text",
      "preview_url": true
    }
  ],
  "status": "status",
  "channel_message_id": "channel_message_id",
  "failure": {
    "code": "code",
    "message": "message"
  },
  "from": "from",
  "from_selection": {
    "reason": "reason"
  }
}
```
