## List chat transcript

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

Lists the chat as a conversation to display: what the customer saw, in
order.

**Behavior**

- The same rows `/events` pages, rebuilt for rendering: each carries a
  `kind` to switch on, the message's canonical `parts`, and the delivery
  state of each outbound message.
- Same paging (`after_seq`, `limit`) and same bounds as `/events`. Use
  this to draw a chat; use `/events` to rebuild state.

### Path Parameters

- `chat: string`

### Query Parameters

- `after_seq: optional number`

  Return rows with `seq` greater than this; omitted (or non-numeric) reads from the start.

- `limit: optional number`

  Page size; absent or outside 1–100 reads as 100.

### Returns

- `data: array of TranscriptRow`

  - `actor: string`

    Who wrote it: `customer`, `partner`, `human`, `flow`, `system` or `brand`.
    On a row carrying a `message_id` this is who COMPOSED that message: a
    human agent replying from a console reads `human`, a deterministic flow
    reads `flow`, and NOT the actor of the underlying event, which is always
    `partner` on a send because that event means we delivered it. Everywhere
    else it is the underlying event's own actor.

  - `created_at: string`

  - `event_type: string`

    The underlying event type (`message_received`, `owner_changed`, `consent_send_refused`, …); render detail from it, but branch on `kind`.

  - `kind: "customer_message" or "business_message" or "system"`

    What KIND of thing happened; switch on this. The set is CLOSED: an event type added later renders as `system`, never as a fourth value.

    - `"customer_message"`

    - `"business_message"`

    - `"system"`

  - `seq: number`

    The `seq` this row is built from; same numbering as the events endpoint, and the transcript's own cursor.

  - `attachments: optional array of object { att_id, mime_type, name, 2 more }`

    Files the CUSTOMER sent with this message; absent when they sent none.

    - `att_id: optional string`

      The retained plaintext's id (`att_…`); absent when nothing was retained.

    - `mime_type: optional string`

    - `name: optional string`

      The file's name as the customer's device sent it.

    - `reason: optional string`

      Present exactly when `att_id` is absent, and a CLOSED set: `too_large` | `brand_unresolved` | `store_unavailable` | `disabled` | `unavailable` (any failure earlier than the retention decision). Never the underlying error text, that is free-form and can carry the channel's own temporary download URL, so it stays on `/events`.

    - `size: optional number`

      Size in bytes.

  - `delivery: optional string`

    That message's delivery state, read from the message row: `queued` | `sending` | `sent` | `retry` | `failed` | `undelivered` | `suppressed`. Two of those are terminal failures, not one: `undelivered` is the "resubmitting later may land" outcome and `failed` the one that will not. There is NO delivered or read receipt here or anywhere on this API; AMB produces neither, and `sent` means only that Apple accepted the send.

  - `form_response: optional unknown`

    A `form_response` row's FULL record, first-class: `values` keyed by your original page ids, `item_ids`, `status`, `sources`, `opened_at`, `closed_at`. Present only on `event_type: "form_response"` rows.

  - `group_id: optional string`

    Ties a fan-out member to its group, and a `form_response` row to the degraded prompt messages that asked, so grouping needs no payload spelunking. Present on rows that belong to a group.

  - `message_id: optional string`

    The outbound message this row is about; pass it to `GET /v1/messages/{msg}`.

  - `operator: optional string`

    WHICH human. On a row carrying a `message_id` this is the operator who
    composed that send, where one did and where the send named them. On every
    other row it is the events endpoint's `operator`: the human we recorded,
    with the same rules.

    **On message rows this deliberately does NOT match `/events`**, which
    shows no `operator` for the same `seq`: a send is recorded by the worker
    that delivered it, with actor `partner` and no operator, so the composer
    is only knowable from the message row. Absence never means "no human";
    it means this row cannot say which.

  - `parts: optional array of unknown`

    The message's canonical parts (the same array a send takes), absent on
    system rows and on a message that carried none: an invitation delivery
    records its invitation identity rather than parts. Typed as an opaque
    array rather than `Part[]` on purpose: the generated Go transport must
    never contain a second decoder for the part union (`content.ParseParts`
    is the one decode door), and a response `$ref` to `Part` would put one
    there.

  - `reaction: optional object { action, correlation, kind, 7 more }`

    The reaction this customer text row IS, when the text is a tapback. The row keeps its text verbatim and `kind` stays `customer_message`, so a client ignoring this key sees exactly the transcript it saw before.

    - `action: "added" or "removed"`

      Whether the customer APPLIED this reaction or TOOK IT AWAY. CLOSED: `added` or `removed`, and always present. Apple sends a removal as its own message, so a removal is its own transcript fact rather than the absence of one. Note what this does NOT tell you: replacing one reaction with another sends the new `added` and no `removed` beside it, so state is last-one-wins per (message, customer) and a client that appends will accumulate reactions the customer no longer has.

      - `"added"`

      - `"removed"`

    - `correlation: "inferred" or "ambiguous" or "unmatched" or "unavailable"`

      HOW the target was reached. CLOSED: `inferred` (exactly one message in the recent window carried that text, and both target keys name it), `ambiguous` (several did; `candidate_count` says how many, and no winner is picked for you), `unmatched` (none did), `unavailable` (Apple named no target at all, which is what a reaction to a non-text message looks like). There is no `exact`, because no reaction on this wire carries an id.

      - `"inferred"`

      - `"ambiguous"`

      - `"unmatched"`

      - `"unavailable"`

    - `kind: "like" or "love" or "dislike" or 5 more`

      What the customer did. CLOSED: `like`, `love`, `dislike`, `laugh`, `emphasize`, `question`, `sticker` (a sticker or Memoji), or `emoji` (any other character, which rides `emoji`).

      - `"like"`

      - `"love"`

      - `"dislike"`

      - `"laugh"`

      - `"emphasize"`

      - `"question"`

      - `"sticker"`

      - `"emoji"`

    - `attachment_id: optional string`

      The sticker's image, on a sticker whose image we retained: pass it to `GET /v1/attachments/{att}/content`. Present ONLY when `kind` is `sticker` and `action` is `added`. Absent when the image was not retained, or when more than one file arrived and the wire does not say which is the sticker: no handle is better than the wrong one. Do not assume a format. Captures show both `image/png` and `image/heic`; read the response's own content type. A PRESENT ID IS NOT A PROMISE OF BYTES, exactly as for `att_id`: retained attachments are swept after their published TTL and the event is immutable, so an old reaction still names the id it was minted with and the content read answers `404`. Handle that; it is the ordinary outcome for anything old enough.

    - `candidate_count: optional number`

      How many messages carried that exact text, on `ambiguous`.

    - `emoji: optional string`

      The character the customer picked, present only when `kind` is `emoji`, where the character IS the reaction and no named one exists for it.

    - `quoted_text: optional string`

      The original message's text as Apple quoted it back: the only handle this wire offers, and what the correlation is made against. Absent when Apple named no target.

    - `target_msg_id: optional string`

      The message reacted to, on `inferred`. Pass it to `GET /v1/messages/{msg}`.

    - `target_seq: optional number`

      That message's `seq`, on `inferred`: the transcript row to scroll to.

    - `target_side: optional "business" or "customer"`

      WHOSE message was reacted to, on `inferred`: `business` or `customer`. A customer can react to their own message, so this is not derivable from the reaction being a customer event. Unlike the correlation it is not an inference; it is the matched row's own type.

      - `"business"`

      - `"customer"`

  - `reason: optional string`

    The reason the event states, where it states one: the failure discriminator on a failed or suppressed send, the handoff reason on an ownership change, the refusal reason on a consent row.

  - `reply: optional unknown`

    A customer's normalized interactive selection (quick reply, list pick, time slot); absent when they sent plain text. A customer who taps a Quick Reply sends no text, so this is the row's only content.

  - `request_id: optional string`

    The `requestIdentifier` this message was delivered under: the exact value an interactive reply names in its own `request_id`, so an answer can be matched to the message it answers instead of inferred from item ids (the same card sent twice defines the same item ids, while each interactive send is delivered under a fresh identifier). Present only on a row naming a message that recorded one: interactive sends mint one, plain sends do not, and rows from before this field was served carry none.

### Example

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

#### Response

```json
{
  "data": [
    {
      "seq": 13,
      "kind": "customer_message",
      "event_type": "message_received",
      "actor": "customer",
      "created_at": "2026-08-06T14:01:58Z",
      "parts": [
        {
          "type": "text",
          "body": "where is my driver?"
        }
      ]
    }
  ]
}
```
