# Event Log

## List account events

**get** `/v1/event_log`

Reads the durable account-wide event sequence in committed order. This
bounded JSON collection is the recovery companion to the SSE stream.
Its opaque cursor is scoped to this account, key mode and collection;
it is not interchangeable with an SSE decimal id or chat cursor.

### Query Parameters

- `cursor: optional string`

- `limit: optional number`

### Returns

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

  - `data: array of AccountEvent`

    - `id: string`

    - `data: object { message }`

      Extensible event-specific data.

      - `message: optional Message`

        Frozen message snapshot on newly materialized incoming-message events, including canonical parts and available authenticated attachment URLs. Older retained events omit it; use the chat transcript for history.

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

    - `timestamp: string`

    - `type: string`

  - `has_more: boolean`

  - `next_cursor: string`

    Exclusive opaque cursor after the last event returned, or null on an empty page.

### Example

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

#### Response

```json
{
  "data": [
    {
      "id": "id",
      "data": {
        "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"
        }
      },
      "timestamp": "2019-12-27T18:11:19.117Z",
      "type": "type"
    }
  ],
  "has_more": true,
  "next_cursor": "next_cursor"
}
```

## Domain Types

### Account Event

- `AccountEvent object { id, data, timestamp, type }`

  One canonical account event envelope. Account sequence is transport metadata and is not a member.

  - `id: string`

  - `data: object { message }`

    Extensible event-specific data.

    - `message: optional Message`

      Frozen message snapshot on newly materialized incoming-message events, including canonical parts and available authenticated attachment URLs. Older retained events omit it; use the chat transcript for history.

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

  - `timestamp: string`

  - `type: string`

### Account Event Page

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

  - `data: array of AccountEvent`

    - `id: string`

    - `data: object { message }`

      Extensible event-specific data.

      - `message: optional Message`

        Frozen message snapshot on newly materialized incoming-message events, including canonical parts and available authenticated attachment URLs. Older retained events omit it; use the chat transcript for history.

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

    - `timestamp: string`

    - `type: string`

  - `has_more: boolean`

  - `next_cursor: string`

    Exclusive opaque cursor after the last event returned, or null on an empty page.
