> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tai42.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Read a conversation thread's transcript

> One thread's records under ``route_name``, one page at a time.

``order`` picks the direction: ``asc`` (the default) reads the transcript oldest first,
``desc`` reads it newest first, which is the order a live tail wants because page 1 then
always holds the latest messages. ``page``/``page_size`` window that order from its own
end, so page 1 of ``desc`` is the newest page and never the oldest.

``q`` filters to records whose inbound text or answer contains that substring — a BOUNDED
scan (the searched text lives inside the record content blob), so a page that spent its
scan budget answers ``truncated: true`` LOUDLY. A ``q`` read never 404s: the unknown-thread
404 below is gated on an UNFILTERED read, so under ``q`` an unknown thread and one that
matched nothing alike read as an EMPTY page.

An admin reads whole records; a non-admin reads the caller-safe projection, which
withholds the internal detail of the route key's run. An unknown ``route_name`` is a loud
404. A thread that is absent or keyed to another route answers the uniform thread
not-found. A ``page`` or ``page_size`` below 1, a ``page`` above the served maximum, a
blank ``thread_id`` or an unknown ``order`` is a 400.

A thread the index still holds but whose rows have expired under the retention TTL is
NOT that 404: it reads as an empty page carrying the indexed ``total``, until the prune
pass reclaims the members and the thread becomes unknown.

Returns ``{"items", "total", "page", "page_size", "next_page", "order", "truncated"}``,
where ``total`` counts the thread's indexed records for an unfiltered read, or the matches
a bounded ``q`` scan found.



## OpenAPI

````yaml /openapi.json get /api/conversations/{route_name}/transcript
openapi: 3.1.0
info:
  description: The operator HTTP surface served under /api/*.
  title: tai42-skeleton API
  version: 14.0.2
servers: []
security: []
paths:
  /api/conversations/{route_name}/transcript:
    get:
      tags:
        - conversations
      summary: Read a conversation thread's transcript
      description: >-
        One thread's records under ``route_name``, one page at a time.


        ``order`` picks the direction: ``asc`` (the default) reads the
        transcript oldest first,

        ``desc`` reads it newest first, which is the order a live tail wants
        because page 1 then

        always holds the latest messages. ``page``/``page_size`` window that
        order from its own

        end, so page 1 of ``desc`` is the newest page and never the oldest.


        ``q`` filters to records whose inbound text or answer contains that
        substring — a BOUNDED

        scan (the searched text lives inside the record content blob), so a page
        that spent its

        scan budget answers ``truncated: true`` LOUDLY. A ``q`` read never 404s:
        the unknown-thread

        404 below is gated on an UNFILTERED read, so under ``q`` an unknown
        thread and one that

        matched nothing alike read as an EMPTY page.


        An admin reads whole records; a non-admin reads the caller-safe
        projection, which

        withholds the internal detail of the route key's run. An unknown
        ``route_name`` is a loud

        404. A thread that is absent or keyed to another route answers the
        uniform thread

        not-found. A ``page`` or ``page_size`` below 1, a ``page`` above the
        served maximum, a

        blank ``thread_id`` or an unknown ``order`` is a 400.


        A thread the index still holds but whose rows have expired under the
        retention TTL is

        NOT that 404: it reads as an empty page carrying the indexed ``total``,
        until the prune

        pass reclaims the members and the thread becomes unknown.


        Returns ``{"items", "total", "page", "page_size", "next_page", "order",
        "truncated"}``,

        where ``total`` counts the thread's indexed records for an unfiltered
        read, or the matches

        a bounded ``q`` scan found.
      operationId: get_api_conversations_route_name_transcript
      parameters:
        - in: path
          name: route_name
          required: true
          schema:
            type: string
        - description: 1-based page number, in the requested ``order``.
          in: query
          name: page
          required: false
          schema:
            default: 1
            maximum: 1000000
            minimum: 1
            title: Page
            type: integer
        - description: Items per page. A larger value is capped to 200, never refused.
          in: query
          name: pageSize
          required: false
          schema:
            default: 50
            minimum: 1
            title: Pagesize
            type: integer
        - description: The thread to read, as the send door returned it.
          in: query
          name: thread_id
          required: true
          schema:
            minLength: 1
            pattern: \S
            title: Thread Id
            type: string
        - description: >-
            ``asc`` reads the transcript oldest first; ``desc`` is the live-tail
            order.
          in: query
          name: order
          required: false
          schema:
            default: asc
            enum:
              - asc
              - desc
            title: Order
            type: string
        - description: >-
            Optional text filter — keep only records whose inbound text or
            answer contains this substring (a BOUNDED scan, so a filtered page
            may report ``truncated``).
          in: query
          name: q
          required: false
          schema:
            title: Q
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    $ref: '#/components/schemas/TranscriptEnvelope'
                required:
                  - data
                type: object
          description: Success.
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Malformed request.
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Missing or invalid api key.
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Resource not found.
        '501':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Error.
      security:
        - ApiKeyAuth: []
components:
  schemas:
    TranscriptEnvelope:
      description: >-
        A page of a thread's records (admin full records or the caller_view
        subset).

        ``order`` is the direction served; ``next_page`` is ``null`` on the last
        page.
      properties:
        items:
          items:
            $ref: '#/components/schemas/ConversationRecordView'
          title: Items
          type: array
        next_page:
          anyOf:
            - type: integer
            - type: 'null'
          title: Next Page
        order:
          title: Order
          type: string
        page:
          title: Page
          type: integer
        page_size:
          title: Page Size
          type: integer
        total:
          title: Total
          type: integer
        truncated:
          title: Truncated
          type: boolean
      required:
        - items
        - total
        - page
        - page_size
        - next_page
        - order
        - truncated
      title: TranscriptEnvelope
      type: object
    Error:
      properties:
        code:
          description: >-
            Stable machine-readable reason a client keys a dedicated error state
            on, present on refusals that opt in (e.g. a 501 not-configured
            refusal). Optional: absent when the error carries only a
            human-readable message.
          type: string
        error:
          type: string
      required:
        - error
      type: object
    ConversationRecordView:
      description: >-
        A conversation answer record as a read door serves it. An admin read
        carries every

        field; the caller-scoped read withholds the route-key's internal detail
        — ``channel``,

        ``our_identity``, ``provider_message_id``, ``callback_url``, ``error``,

        ``outbound_message_ids`` and ``attempts`` are present only for an admin
        caller and are

        absent from the caller_view subset.
      properties:
        answer:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Answer
        answer_parts:
          anyOf:
            - items:
                $ref: '#/components/schemas/AnswerPart'
              type: array
            - type: 'null'
          default: null
          title: Answer Parts
        answer_status:
          anyOf:
            - enum:
                - answered
                - error
                - silent
              type: string
            - type: 'null'
          default: null
          title: Answer Status
        attempts:
          anyOf:
            - type: integer
            - type: 'null'
          default: null
          title: Attempts
        callback_url:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Callback Url
        caller_principal:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Caller Principal
        channel:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Channel
        client_address:
          maxLength: 256
          minLength: 1
          title: Client Address
          type: string
        created_at:
          title: Created At
          type: number
        delivery_status:
          $ref: '#/components/schemas/DeliveryStatus'
          default: pending_delivery
        door:
          enum:
            - api
            - channel
          title: Door
          type: string
        error:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Error
        inbound_attachments:
          anyOf:
            - items:
                $ref: '#/components/schemas/MediaItem'
              type: array
            - type: 'null'
          default: null
          title: Inbound Attachments
        inbound_event:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          default: null
          title: Inbound Event
        inbound_form:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          default: null
          title: Inbound Form
        inbound_kind:
          default: message
          enum:
            - message
            - event
          title: Inbound Kind
          type: string
        inbound_locale:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Inbound Locale
        inbound_location:
          anyOf:
            - $ref: '#/components/schemas/LocationElement'
            - type: 'null'
          default: null
        inbound_text:
          title: Inbound Text
          type: string
        message_id:
          minLength: 1
          title: Message Id
          type: string
        origin:
          enum:
            - client
            - operator
          title: Origin
          type: string
        our_identity:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Our Identity
        outbound_message_ids:
          anyOf:
            - items:
                type: string
              type: array
            - type: 'null'
          default: null
          title: Outbound Message Ids
        provider_message_id:
          anyOf:
            - minLength: 1
              type: string
            - type: 'null'
          default: null
          title: Provider Message Id
        route_name:
          minLength: 1
          title: Route Name
          type: string
        submitted_by:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Submitted By
        thread_id:
          minLength: 1
          title: Thread Id
          type: string
        updated_at:
          title: Updated At
          type: number
      required:
        - message_id
        - route_name
        - door
        - thread_id
        - client_address
        - origin
        - inbound_text
        - created_at
        - updated_at
      title: ConversationRecordView
      type: object
    AnswerPart:
      additionalProperties: false
      description: >-
        One message of an ordered multi-message answer — the platform's rich
        part shape.


        It mirrors :class:`ChannelNotification`'s CONTENT surface exactly
        (``message`` plus the

        optional ``media`` / ``location`` / ``template`` / ``options`` /
        ``sections`` / ``header``

        / ``footer`` / ``schema`` richer-send forms and their validators), MINUS
        the per-delivery

        routing fields (``recipient`` / ``sender_identity``) — those stay on the
        single delivery,

        never per part. The delivery machine sends each part as its own
        ``ChannelNotification``,

        in order, chunking the ``message`` text at the channel width and
        carrying the part's

        richer forms alongside it, exactly as a single notification does today.
        Every

        cross-field rule (content-only blank message, ``options`` XOR
        ``sections``, ``schema``

        excludes both, ``header``/``footer`` require a choice surface,
        ``template`` standalone) is

        the SHARED
        :func:`~tai42_contract.channels.check_interactive_composition`, so an
        authored

        part and a delivered notification can never diverge.


        A part is a NEW authoring surface, so it is STRICT FROM BIRTH: unknown
        keys are refused

        (``extra="forbid"``) rather than silently dropped. A tool route authors
        parts as a JSON

        array whose elements are each EITHER a plain string (shorthand for a
        text-only part) or a

        part object — both normalize to THIS one model. Frozen.


        ``message`` mirrors :class:`ChannelNotification.message`'s
        blank-vs-media RULE: non-blank BY

        DEFAULT, EXCEPT it may be blank for a MEDIA-ONLY part — a caption-less
        image with no text

        carrier. The admissible states are "``message`` non-blank" OR "blank
        ``message`` WITH non-empty

        ``media``"; a blank ``message`` and no media has nothing to deliver and
        is refused. ``options``

        REQUIRE a non-blank ``message`` (a tappable choice needs a prompt), and
        a ``template`` rides a

        non-blank ``message`` too — so a media-only part carries only ``media``.
        Unlike

        ``ChannelNotification`` (always constructed in code), a part is authored
        as JSON where the

        ``message`` key may be ABSENT, so it defaults to ``""`` — a media-only
        part is just ``{"media": …}``.
      properties:
        data:
          anyOf:
            - $ref: '#/components/schemas/FormData'
            - type: 'null'
          default: null
        footer:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Footer
        header:
          anyOf:
            - $ref: '#/components/schemas/MediaItem'
            - type: 'null'
          default: null
        location:
          anyOf:
            - $ref: '#/components/schemas/LocationElement'
            - type: 'null'
          default: null
        media:
          anyOf:
            - items:
                $ref: '#/components/schemas/MediaItem'
              type: array
            - type: 'null'
          default: null
          title: Media
        message:
          default: ''
          title: Message
          type: string
        options:
          anyOf:
            - items:
                discriminator:
                  mapping:
                    link:
                      $ref: '#/components/schemas/LinkOption'
                    reply:
                      $ref: '#/components/schemas/ReplyOption'
                  propertyName: kind
                oneOf:
                  - $ref: '#/components/schemas/ReplyOption'
                  - $ref: '#/components/schemas/LinkOption'
              type: array
            - type: 'null'
          default: null
          title: Options
        pages:
          anyOf:
            - items:
                $ref: '#/components/schemas/FormPage'
              type: array
            - type: 'null'
          default: null
          title: Pages
        schema:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          default: null
          title: Schema
        sections:
          anyOf:
            - items:
                $ref: '#/components/schemas/OptionSection'
              type: array
            - type: 'null'
          default: null
          title: Sections
        template:
          anyOf:
            - $ref: '#/components/schemas/ChannelTemplate'
            - type: 'null'
          default: null
      title: AnswerPart
      type: object
    DeliveryStatus:
      description: >-
        Where a record sits between intake and a terminal outcome.


        ``accepted`` is pre-turn intake and carries no answer;
        ``pending_delivery`` is

        persisted-but-unsent (what a re-drive resumes); ``provisional`` is sent
        and awaiting an

        out-of-band receipt or grace expiry;
        ``delivered``/``failed``/``shed``/``silent`` are

        terminal and are the only states carrying the retention TTL. ``shed``
        ran no turn and

        never sends; ``silent`` ran a tool turn whose reply mapped to nothing
        and so, by

        design, sends nothing; ``delivered`` on an api record without a callback
        means the

        answer is readable at the message door and nothing was sent.
      enum:
        - accepted
        - pending_delivery
        - provisional
        - delivered
        - failed
        - shed
        - silent
      title: DeliveryStatus
      type: string
    MediaItem:
      description: >-
        One media item shown WITH a message — a display element, and inbound the
        shape a participant's

        sent media takes.


        ``kind`` selects how it renders: an ``image`` inline, a
        ``document``/``video``/``audio`` as

        the matching file bubble, a ``link`` as a labelled anchor. ``url`` is
        the source. A file

        kind (``image``/``document``/``video``/``audio``) must be an absolute
        ``https`` URL, a

        same-origin ``{MEDIA_ROUTE_PREFIX}{id}`` reference to media the skeleton
        serves by id, or an

        absolute ``http(s)`` served reference of that same
        ``{MEDIA_ROUTE_PREFIX}{id}`` path a

        channel send mints from ``public_base_url`` (remote file media is
        https-only: the inbox CSP

        ``img-src`` admits ``https:``/``data:`` and same-origin but not
        ``http:``, so an ``http:``

        remote source would be an unrenderable record); ``image`` ADDITIONALLY
        admits an inline

        ``data:image/*`` URI — that inline form is image-only, a ``data:`` URI
        on any other file

        kind is refused. A ``link`` must be an absolute ``http(s)`` URL (anchors
        are not governed by

        ``img-src``; the human clicks through). A remote url names a host
        directly — an ASCII DNS

        name, dotted-quad IPv4, or bracketed IPv6 (IDN callers supply punycode);
        an embedded

        ``user@`` credential form is rejected as it spoofs the authority — and
        is always a single

        line — raw whitespace and control/format characters are rejected.
        ``caption`` is the

        accessibility text — the image's alt text, a file's label, or the link's
        display label.

        ``filename`` is the document's suggested display name; it is meaningful
        ONLY for a

        ``document`` item and is refused on every other kind.
      properties:
        caption:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Caption
        filename:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Filename
        kind:
          $ref: '#/components/schemas/MediaKind'
        url:
          title: Url
          type: string
      required:
        - kind
        - url
      title: MediaItem
      type: object
    LocationElement:
      description: >-
        A geographic point shared on a message — the one shape used BOTH ways:
        an outbound place a

        flow shares and the inbound location a participant sent.


        ``latitude``/``longitude`` are WGS84 decimal degrees, bounded to their
        valid ranges

        (latitude -90..90, longitude -180..180). ``name`` is an optional place
        label and ``address``

        an optional street address, each a single-line non-blank string within
        its cap when present

        (raw whitespace and control/format characters are rejected, as on a
        media url — an embedded

        newline or bidi spoof would corrupt the rendered pin label). A channel
        that cannot render a

        map renders the coordinates (and any name/address) as text. Frozen.
      properties:
        address:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Address
        latitude:
          title: Latitude
          type: number
        longitude:
          title: Longitude
          type: number
        name:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Name
      required:
        - latitude
        - longitude
      title: LocationElement
      type: object
    FormData:
      description: >-
        Per-send data layered over a form's published schema for ONE send.


        ``values`` prefills top-level properties — each entry keyed by property
        name,

        its value shown filled in and validated against that property's schema.

        ``options`` supplies a per-send choice list for a property whose schema
        is a

        string (or an array of strings), keyed by property name: the list
        REPLACES that

        property's ``enum`` for this send only (labels shown, values submitted).
        The

        model holds only the shape; the cross-check against the schema (unknown

        property, a value that fails its schema, options on a non-string
        property, an

        empty list) is done once by the interaction request. Frozen.
      properties:
        options:
          additionalProperties:
            items:
              $ref: '#/components/schemas/FormOption'
            type: array
          default: {}
          title: Options
          type: object
        values:
          additionalProperties: true
          default: {}
          title: Values
          type: object
      title: FormData
      type: object
    LinkOption:
      description: >-
        A tappable link action. Tapping OPENS ``url`` (an absolute ``http(s)``
        URL) in the human's

        browser — NO message is submitted, distinct from a :class:`ReplyOption`.
        ``label`` is the

        button text. The URL-button / call-to-action case. Frozen.
      properties:
        kind:
          const: link
          default: link
          title: Kind
          type: string
        label:
          title: Label
          type: string
        url:
          title: Url
          type: string
      required:
        - label
        - url
      title: LinkOption
      type: object
    ReplyOption:
      description: >-
        A tappable suggested reply. Tapping SUBMITS ``text`` as the
        participant's next inbound message —

        the quick-reply / list-row case, where the option's own text becomes the
        turn. ``description``

        is an OPTIONAL secondary line a sectioned-list row renders under its
        ``text``; a channel that

        renders flat buttons (no descriptions) ignores it.


        ``id`` is an OPTIONAL author-set stable identifier for the button/list
        row. When set, a channel

        sends it verbatim on the wire and the participant's tap echoes it back
        (a channel surfaces the echoed

        id to the inbound turn as opaque enrichment — e.g. Slack forwards it as
        ``params.reply_id``);

        when ``None`` the channel mints its own id as today. Bounded by
        ``OPTION_ID_MAX_CHARS`` and a

        single-line non-blank label — the strictest carrier's rule. Frozen.
      properties:
        description:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Description
        id:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Id
        kind:
          const: reply
          default: reply
          title: Kind
          type: string
        text:
          title: Text
          type: string
      required:
        - text
      title: ReplyOption
      type: object
    FormPage:
      description: >-
        One step of a stepped form: ``title`` heads the step and ``fields``
        names the

        top-level properties shown on it. Across a form's ``pages`` every
        property

        appears exactly once (the interaction request enforces the coverage);
        absent

        ``pages`` means one page. Frozen.
      properties:
        fields:
          items:
            type: string
          title: Fields
          type: array
        title:
          title: Title
          type: string
      required:
        - title
        - fields
      title: FormPage
      type: object
    OptionSection:
      description: >-
        One titled section of a sectioned option list. ``title`` is the section
        header; ``rows`` are

        its entries — a sectioned list holds :class:`ReplyOption` rows ONLY (a
        tapped row submits its

        text; a link action is a button, never a list row). A present ``rows``
        is non-empty. Frozen.
      properties:
        rows:
          items:
            $ref: '#/components/schemas/ReplyOption'
          title: Rows
          type: array
        title:
          title: Title
          type: string
      required:
        - title
        - rows
      title: OptionSection
      type: object
    ChannelTemplate:
      description: >-
        A pre-approved, named template a channel sends outside its freeform
        window.


        Some media only accept arbitrary text inside a bounded conversation
        window (e.g. WhatsApp's

        24-hour customer-service window); outside it, the sole accepted send is
        an operator-authored

        template referenced by ``name`` in an approved ``language`` — both
        required and non-blank.


        The template's runtime arguments are its NAMED components (never one
        flat positional list):


        * ``header_media`` — the media argument for a media HEADER component (a
        display item:
          image/document/video/audio, never a ``link``); ``None`` when the template has no media
          header (or a static/text header needing no argument).
        * ``body_parameters`` — the POSITIONAL body-text values substituted into
        the body's
          placeholders in order; empty when the body has no placeholders. Typed values (currency,
          date-time) ride as their pre-formatted STRING here — the contract does not model the type.
        * ``buttons`` — the POSITIONAL per-button arguments of the buttons
        component
          (:data:`TemplateButtonParam`: a :class:`QuickReplyButtonParam` payload or a
          :class:`UrlButtonParam` url suffix); the i-th entry parameterises the i-th button, at most
          ``TEMPLATE_BUTTONS_MAX``. Empty when no button needs a runtime argument.
      properties:
        body_parameters:
          items:
            type: string
          title: Body Parameters
          type: array
        buttons:
          items:
            discriminator:
              mapping:
                quick_reply:
                  $ref: '#/components/schemas/QuickReplyButtonParam'
                url:
                  $ref: '#/components/schemas/UrlButtonParam'
              propertyName: kind
            oneOf:
              - $ref: '#/components/schemas/QuickReplyButtonParam'
              - $ref: '#/components/schemas/UrlButtonParam'
          title: Buttons
          type: array
        header_media:
          anyOf:
            - $ref: '#/components/schemas/MediaItem'
            - type: 'null'
          default: null
        language:
          title: Language
          type: string
        name:
          title: Name
          type: string
      required:
        - name
        - language
      title: ChannelTemplate
      type: object
    MediaKind:
      enum:
        - image
        - link
        - document
        - video
        - audio
      title: MediaKind
      type: string
    FormOption:
      description: >-
        One per-send choice for a form field: ``value`` is the string submitted
        as

        the answer, ``label`` (when set) is shown to the human in its place. A
        per-send

        option list REPLACES a property's schema ``enum`` for ONE send — the
        published

        form is unchanged, so a variant needs no re-publish. Frozen.
      properties:
        label:
          anyOf:
            - type: string
            - type: 'null'
          default: null
          title: Label
        value:
          title: Value
          type: string
      required:
        - value
      title: FormOption
      type: object
    QuickReplyButtonParam:
      description: >-
        The runtime argument for one QUICK-REPLY button of a template's buttons
        component:

        ``payload`` is the string the medium returns when the human taps the
        button. Frozen.
      properties:
        kind:
          const: quick_reply
          default: quick_reply
          title: Kind
          type: string
        payload:
          title: Payload
          type: string
      required:
        - payload
      title: QuickReplyButtonParam
      type: object
    UrlButtonParam:
      description: >-
        The runtime argument for one URL button of a template's buttons
        component:

        ``url_parameter`` is the dynamic suffix substituted into the button's
        pre-approved URL.

        Frozen.
      properties:
        kind:
          const: url
          default: url
          title: Kind
          type: string
        url_parameter:
          title: Url Parameter
          type: string
      required:
        - url_parameter
      title: UrlButtonParam
      type: object
  securitySchemes:
    ApiKeyAuth:
      in: header
      name: x-api-key
      type: apiKey

````