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

> ## Agent Instructions
> The Relay API base URL is https://api.relayapp.im. Never use workers.dev origins.
> The contract is raw HTTPS and JSON at https://api.relayapp.im. The one optional published package is @relaymessenger/cli. Import nothing else.
> One send is one message. Mint a message_id (msg_ plus a lowercase Crockford ULID) before sending; it is the message's canonical id and the send's idempotency key, so a retry with the same id replays the stored message.
> Message content is immutable. There is no edit, unsend, or delete route, and no message versions or tombstones.
> A reply is a pointer: reply_to is { message_id, part_id? } and the client draws the quote from the target.
> Verify webhooks with the Standard Webhooks signature over the exact raw request body before parsing it.
> Webhooks and GET /v1/events read the same durable log and can run at once. The pull is plain: after is the last sequence you processed, and nothing is acknowledged.
> An agent in a group is an ordinary member. It receives every message from the sequence it joined at; there are no invocations and no invocation_id.

# Send a message into a conversation

> The conversation-scoped form of a send, with the same body and the same idempotency rules as `POST /v1/messages`. It answers `201` and wraps the committed message in an array of one, which is the shape shipped clients read; `/v2` returns the message on its own. Both commit exactly one message.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/chats/{chat_id}/messages
openapi: 3.1.0
info:
  title: Relay developer API
  version: '0.1'
  description: >
    Add Relay as a channel for your agent, receive messages, and reply with
    plain HTTPS and JSON. Authenticate every request with an Agent Token unless
    the endpoint is marked otherwise. Data parts are stored and delivered as
    sent and render through their fallback text, except for the reserved
    `data.type` values Relay resolves itself.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://api.relayapp.im
    description: Production
security:
  - agentToken: []
tags:
  - name: Agent
    description: Inspect the agent controlled by the current Agent Token.
  - name: Messages
    description: Send a message, read history, and react.
  - name: Conversations
    description: >
      List the conversations an agent is in, advance receipts, and show the
      typing indicator.
  - name: Events
    description: >
      Receive durable inbound events, either through signed webhook receivers or
      by pulling `GET /v1/events`. Both work at once: one durable log per agent
      feeds both transports. Delivery is at least once everywhere; always
      deduplicate by `event_id`.
  - name: Attachments
    description: Upload bytes once and reference them from any number of parts.
  - name: Pairing
    description: >
      Device authorization (RFC 8628) for terminal bridges. Four calls, in this
      order: the computer asks for a code, the person opens the verification URI
      so the code is claimed for their account, the person approves it, and the
      computer exchanges the code for a session it uses to provision its agent
      and mint that agent's token. The token never travels to the phone.
  - name: Public
    description: Share profiles.
paths:
  /v1/chats/{chat_id}/messages:
    post:
      tags:
        - Messages
      summary: Send a message into a conversation
      description: >
        The conversation-scoped form of a send, with the same body and the same
        idempotency rules as `POST /v1/messages`. It answers `201` and wraps the
        committed message in an array of one, which is the shape shipped clients
        read; `/v2` returns the message on its own. Both commit exactly one
        message.
      operationId: sendConversationMessage
      parameters:
        - $ref: '#/components/parameters/ConversationId'
        - $ref: '#/components/parameters/OptionalIdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageBody'
      responses:
        '201':
          description: The committed message, as an array of one.
          content:
            application/json:
              schema:
                type: object
                required:
                  - messages
                properties:
                  messages:
                    type: array
                    minItems: 1
                    maxItems: 1
                    items:
                      $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: The sender is not an active participant of this conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >-
            `idempotency_conflict`: this `message_id` belongs to a different
            request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The request body exceeds 512 KB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            The message id, parts, an attachment reference, or the reply target
            is invalid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/CommitUnavailable'
      security:
        - agentToken: []
        - userSession: []
components:
  parameters:
    ConversationId:
      name: chat_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^cnv_[0-9a-hjkmnp-tv-z]{26}$
      example: cnv_01k1m4q9vn2r7t9b4c6qdh8xwy
    OptionalIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        pattern: ^msg_[0-9a-hjkmnp-tv-z]{26}$
      description: >
        A legacy way to carry `message_id` for clients that cannot put it in the
        body. It is read only when it is itself a `msg_` id; any other value is
        ignored and the server mints the id.
  schemas:
    SendMessageBody:
      type: object
      description: >
        The `/v1` send body. Unknown fields are ignored rather than refused;
        `/v2` refuses them.
      required:
        - parts
      properties:
        message_id:
          type: string
          pattern: ^msg_[0-9a-hjkmnp-tv-z]{26}$
          example: msg_01k1m4q9vn2r7t9b4c6qdh8xwy
          description: >
            The client-minted canonical id and idempotency key. Omit it and
            Relay mints one, which makes the send non-idempotent.
        parts:
          type: array
          minItems: 1
          maxItems: 32
          description: Ordered parts of one message. An absent or empty array returns 422.
          items:
            $ref: '#/components/schemas/PartInput'
        reply_to:
          $ref: '#/components/schemas/ReplyRefInput'
        text:
          type: string
          description: >
            Optional plain-language representation for notifications and search.
            Relay derives one when it is absent.
    Message:
      type: object
      additionalProperties: false
      description: >
        One committed message. Content is immutable: there is no edit, no
        unsend, no version and no tombstone, so a message a client has stored
        never changes underneath it. Only the receipt stamps and the reactions
        move.
      required:
        - id
        - chat_id
        - sequence
        - item_type
        - sender_handle
        - is_from_me
        - parts
        - reply_to
        - text
        - status
        - created_at
      properties:
        id:
          type: string
          pattern: ^msg_[0-9a-hjkmnp-tv-z]{26}$
          example: msg_01k1m4q9vn2r7t9b4c6qdh8xwy
        chat_id:
          type: string
          pattern: ^cnv_
          example: cnv_01k1m4q9vn2r7t9b4c6qdh8xwy
        sequence:
          type: integer
          description: Order inside the conversation. It is unrelated to event sequences.
        item_type:
          type: integer
          enum:
            - 0
            - 1
            - 2
            - 3
          description: >
            chat.db's item_type, with Apple's values: 0 an ordinary message, 1 a
            participant change (`group_action_type` 0 = added, 1 = removed/left;
            `other_handle` = who), 2 a rename (`group_title`), 3 a group-photo
            change. A notice row (item_type ≠ 0) is sent by the person who did
            the thing, carries no parts, and its human line rides in `text`.
            Render it as a centered system line, not as a bubble.
        group_action_type:
          type: integer
          enum:
            - 0
            - 1
        other_handle:
          type: string
          pattern: ^(usr|agt)_
        group_title:
          type: string
        is_audio_message:
          type: boolean
          description: 'chat.db''s is_audio_message: this message is a voice memo.'
        sender_handle:
          $ref: '#/components/schemas/Sender'
        is_from_me:
          type: boolean
          description: >
            True when the caller this payload was projected for is the sender.
            Relay resolves direction on the server and states it here, because
            comparing sender.id against your own identity is ambiguous for a
            caller that does not yet know it, and wrong for every other
            participant in a group.
        parts:
          type: array
          minItems: 0
          maxItems: 32
          description: |
            An item_type 0 message has at least one part; a notice carries none.
          items:
            $ref: '#/components/schemas/Part'
        reply_to:
          $ref: '#/components/schemas/ReplyRef'
        reactions:
          type: array
          description: >
            Current reactions. History and bootstrap projections include this
            array; a send response carries the committed message alone.
          items:
            $ref: '#/components/schemas/ProjectedReaction'
        text:
          type: string
          description: Plain-language representation used by notifications and search.
        status:
          type: string
          enum:
            - sent
            - delivered
            - read
          description: >
            The sender's view of this message in a 1:1 conversation. A group
            message is always `sent`: Relay reports no per-recipient receipts in
            groups, the way iMessage does not.
          example: sent
        delivered_at:
          type: string
          format: date-time
          description: Present in a 1:1 conversation once the recipient has it.
        read_at:
          type: string
          format: date-time
          description: Present in a 1:1 conversation once the recipient has read it.
        created_at:
          type: string
          format: date-time
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              example: invalid_request
            message:
              type: string
              example: chat_id is required
    PartInput:
      oneOf:
        - $ref: '#/components/schemas/TextPartInput'
        - $ref: '#/components/schemas/MediaPartInput'
        - $ref: '#/components/schemas/LinkPreviewPartInput'
        - $ref: '#/components/schemas/DataPartInput'
      discriminator:
        propertyName: type
    ReplyRefInput:
      type:
        - object
        - 'null'
      additionalProperties: false
      description: >
        A reply target on a send: a message of this conversation, optionally one
        exact part of it.
      required:
        - message_id
      properties:
        message_id:
          type: string
          pattern: ^msg_[0-9a-hjkmnp-tv-z]{26}$
          example: msg_01k1m4q9vn2r7t9b4c6qdh8xwy
        part_id:
          $ref: '#/components/schemas/PartId'
    Sender:
      type: object
      additionalProperties: false
      required:
        - kind
        - id
      description: >
        A participant. Every message is sent by a person or an agent; there is
        no system sender, and a group notice is sent by whoever caused it.
      properties:
        kind:
          type: string
          enum:
            - user
            - agent
        id:
          type: string
          pattern: ^(usr|agt)_
          example: usr_01JZU1F0BD
    Part:
      description: >
        One part of a message, discriminated on `type`. The union is
        deliberately open: a client that meets a `type` it does not know must
        render the message's `text` for that part and leave it otherwise
        untouched, rather than dropping the part or failing the message. Relay
        adds part types without a version bump, so this is the difference
        between an old client degrading and an old client breaking.
      oneOf:
        - $ref: '#/components/schemas/TextPart'
        - $ref: '#/components/schemas/MediaPart'
        - $ref: '#/components/schemas/LinkPreviewPart'
        - $ref: '#/components/schemas/DataPart'
        - $ref: '#/components/schemas/UnknownPart'
    ReplyRef:
      type:
        - object
        - 'null'
      additionalProperties: false
      description: >
        A pointer, never a copy. `message_id` names a message of the same
        conversation and `part_id` optionally names one exact part of it. The
        client draws the quote from the target itself, so a reply always shows
        what the target says now, and both ids are permanent.
      required:
        - message_id
      properties:
        message_id:
          type: string
          pattern: ^msg_[0-9a-hjkmnp-tv-z]{26}$
          example: msg_01k1m4q9vn2r7t9b4c6qdh8xwy
        part_id:
          allOf:
            - $ref: '#/components/schemas/PartId'
          description: >
            Present when the reply targeted one exact part. Omitted, not null,
            when the reply names the whole message.
    ProjectedReaction:
      type: object
      additionalProperties: false
      required:
        - target_part_id
        - type
        - actor_kind
        - actor_id
        - created_at
      description: One reaction as it appears in a message's `reactions` array.
      properties:
        target_part_id:
          oneOf:
            - $ref: '#/components/schemas/PartId'
            - type: 'null'
          description: >
            The part this reaction is anchored to, and the only identity for it.
            Null exactly when the reaction is on the whole message.
        type:
          type: string
          enum:
            - love
            - like
            - dislike
            - laugh
            - emphasize
            - question
            - custom
          description: >
            Apple's six tapback verbs (chat.db associated_message_type
            2000–2005, spelled the way Linq spells them) plus `custom`.
        custom_emoji:
          type: string
          maxLength: 32
          description: Present exactly when `type` is `custom`.
        actor_kind:
          type: string
          enum:
            - user
            - agent
        actor_id:
          type: string
          pattern: ^(usr|agt)_
        created_at:
          type: string
          format: date-time
    TextPartInput:
      type: object
      additionalProperties: false
      required:
        - type
        - text
      properties:
        type:
          type: string
          const: text
        text:
          type: string
          minLength: 1
          description: UTF-8 text, limited to 8 KB by the server.
        mention:
          $ref: '#/components/schemas/Mention'
        mention_range:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: integer
            minimum: 0
          description: >
            [start, end) UTF-16 code-unit offsets over `text`: one mention per
            text part, carried the way Linq carries them. The ranged text is
            display text and is not required to spell the handle. Required when
            `mention` is present. A mention notifies the named participant even
            when they have muted the group.
        styles:
          type: array
          maxItems: 200
          description: >
            Formatting runs over `text`. Each range covers a run of UTF-16 code
            units, sorted by `start` and non-overlapping, and names the formats
            applied to that run. Styles are presentation only: the text stays
            canonical without them, and fallback text ignores them. An empty
            array is meaningful and kept. It marks the part as structured plain
            text, distinguishing it from a legacy Markdown body. Omit the field
            entirely for a legacy body.
          items:
            $ref: '#/components/schemas/StyleRange'
    MediaPartInput:
      type: object
      additionalProperties: false
      required:
        - type
      properties:
        type:
          type: string
          const: media
        url:
          type: string
          format: uri
          pattern: ^https://
          description: >
            Public HTTPS source URL. The hostname must be publicly reachable:
            private and reserved ranges, loopback, and IP-literal hostnames are
            rejected. At commit the server imports the bytes into Relay-owned
            attachment storage and rewrites the part to reference the copy, so
            delivered history never depends on the third-party host staying
            alive.
        attachment_id:
          type: string
          pattern: ^att_
          description: Available attachment created by `POST /v1/attachments`.
        content_type:
          type: string
          pattern: ^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*$
          example: video/mp4
          description: >
            MIME type for a public URL. For attachment_id sources, Relay uses
            the MIME type stored at upload and rejects a conflicting value.
        media_kind:
          type: string
          enum:
            - image
            - video
            - audio
            - file
          description: >
            Optional presentation intent. Relay derives this from a specific
            image/*, video/*, or audio/* content_type. Use it to identify
            generic application/octet-stream media.
        filename:
          type: string
          minLength: 1
          maxLength: 180
          example: quarterly-report.pdf
          description: >
            Optional display filename, shown on a file row. Control characters
            are rejected and surrounding whitespace is trimmed. For
            attachment_id sources Relay uses the filename stored at upload and
            ignores this value.
        size_bytes:
          type: integer
          minimum: 0
          maximum: 104857600
          example: 2418562
          description: >
            Optional byte count, shown beside the filename so a reader sees the
            size before downloading. For attachment_id sources Relay uses the
            stored byte count and ignores this value.
        width:
          type: integer
          minimum: 1
          maximum: 100000
          description: >
            Optional pixel width, provided together with height. Clients use the
            pair to reserve the true aspect ratio before the media bytes
            download. When omitted for an uploaded image attachment, the server
            derives both values from the stored bytes.
        height:
          type: integer
          minimum: 1
          maximum: 100000
          description: Optional pixel height, provided together with width.
        blur_hash:
          type: string
          minLength: 6
          maxLength: 96
          pattern: ^[0-9A-Za-z#$%*+,\-.:;=?@\[\]^_{|}~]{6,96}$
          description: >
            Optional blurhash placeholder (base83 characters only, see
            https://blurha.sh). Clients decode it into a soft preview before the
            media bytes download. When omitted for an uploaded image attachment,
            the server derives one from the stored bytes at commit.
      dependentRequired:
        width:
          - height
        height:
          - width
      oneOf:
        - properties:
            url: {}
          required:
            - url
        - properties:
            attachment_id: {}
          required:
            - attachment_id
    LinkPreviewPartInput:
      type: object
      additionalProperties: false
      required:
        - type
        - url
      description: URL rendered as a native rich preview when metadata is available.
      properties:
        type:
          type: string
          const: link
        url:
          type: string
          format: uri
          pattern: ^https://
          maxLength: 2048
        title:
          type: string
          maxLength: 512
          description: >
            Optional page title you resolved for this URL. Relay carries the
            sender's metadata with the message rather than fetching the page for
            each recipient, the way Signal carries an OWSLinkPreviewDraft and
            iMessage carries an LPLinkMetadata, so no recipient opens a
            connection to a URL somebody else chose. Send it with the message:
            Relay never backfills it later.
        description:
          type: string
          maxLength: 512
          description: >
            Optional page description you resolved for this URL, carried with
            the same rules as title.
    DataPartInput:
      type: object
      additionalProperties: false
      required:
        - type
        - data
      properties:
        type:
          type: string
          const: data
        data:
          description: >
            Integration-defined JSON, limited to 16 KB by the server. Relay
            stores and delivers it as sent; clients render a part-level fallback
            string when present, then the message text. Consumers must ignore
            unknown fields inside a data part.

            One `data.type` value is reserved and is not carried through as
            sent: `agent_card` names an agent to share and Relay resolves the
            card itself, so what you send is AgentCardDataInput and what
            recipients receive is AgentCardData.
          anyOf:
            - $ref: '#/components/schemas/AgentCardDataInput'
            - description: Other integration-defined JSON data.
    PartId:
      type: string
      pattern: ^prt_[0-9a-hjkmnp-tv-z]{26}$
      example: prt_01k1m4q9vn2r7t9b4c6qdh8xwy
      description: >
        Permanent part identity, minted when the part is committed. It is what a
        reply or a reaction names, so a target never depends on a position.
    TextPart:
      type: object
      additionalProperties: false
      required:
        - type
        - text
      properties:
        part_index:
          type: integer
          minimum: 0
        part_id:
          $ref: '#/components/schemas/PartId'
        position:
          type: integer
          minimum: 0
        type:
          type: string
          const: text
        text:
          type: string
        mention:
          $ref: '#/components/schemas/Mention'
        mention_range:
          type: array
          minItems: 2
          maxItems: 2
          items:
            type: integer
            minimum: 0
          description: The mention's [start, end) UTF-16 range over `text`.
        styles:
          type: array
          maxItems: 200
          description: >
            Formatting runs carried by this text part, as validated at send
            time. Offsets are UTF-16 code units over `text`. An empty array
            marks structured plain text; the field is absent on legacy Markdown
            bodies.
          items:
            $ref: '#/components/schemas/StyleRange'
    MediaPart:
      type: object
      additionalProperties: false
      required:
        - part_index
        - type
        - url
        - content_type
        - media_kind
      properties:
        part_index:
          type: integer
          minimum: 0
        part_id:
          $ref: '#/components/schemas/PartId'
        position:
          type: integer
          minimum: 0
        type:
          type: string
          const: media
        url:
          type: string
          format: uri
        attachment_id:
          type: string
          pattern: ^att_
        content_type:
          type: string
          description: >
            Canonical MIME type. Attachment-backed parts use the upload's stored
            MIME type.
        media_kind:
          type: string
          enum:
            - image
            - video
            - audio
            - file
          description: Canonical presentation kind used by clients.
        filename:
          type: string
          minLength: 1
          maxLength: 180
          example: quarterly-report.pdf
          description: >
            Display filename, present when the part is backed by an attachment
            that carries one or the sender declared one. An attachment's stored
            filename wins over a declared value.
        size_bytes:
          type: integer
          minimum: 0
          maximum: 104857600
          example: 2418562
          description: >
            Byte count, present under the same rule as filename. For an
            attachment this is the stored byte count, so a client can show the
            true size beside a file row before downloading.
        width:
          type: integer
          minimum: 1
          description: >
            Pixel width, present when the sender declared dimensions or the
            server derived them from an uploaded image. Always paired with
            height; reserve the balloon at width/height before downloading.
        height:
          type: integer
          minimum: 1
        blur_hash:
          type: string
          minLength: 6
          maxLength: 96
          pattern: ^[0-9A-Za-z#$%*+,\-.:;=?@\[\]^_{|}~]{6,96}$
          description: >
            Blurhash placeholder, present when the sender declared one or the
            server derived it from a stored image attachment. Decode it into a
            soft preview to draw before the media bytes download.
    LinkPreviewPart:
      type: object
      additionalProperties: false
      required:
        - type
        - url
      properties:
        part_index:
          type: integer
          minimum: 0
        part_id:
          $ref: '#/components/schemas/PartId'
        position:
          type: integer
          minimum: 0
        type:
          type: string
          const: link
        url:
          type: string
          format: uri
          maxLength: 2048
        title:
          type: string
          maxLength: 512
          description: >
            Page title the sender resolved, present only when the sender sent
            one. Relay carries the sender's metadata and never fetches the page
            on a recipient's behalf, so a part with neither title nor
            description renders as a plain link.
        description:
          type: string
          maxLength: 512
          description: >
            Page description the sender resolved, present under the same rule as
            title.
    DataPart:
      type: object
      additionalProperties: false
      required:
        - type
        - data
      properties:
        part_index:
          type: integer
          minimum: 0
        part_id:
          $ref: '#/components/schemas/PartId'
        position:
          type: integer
          minimum: 0
        type:
          type: string
          const: data
        data:
          anyOf:
            - $ref: '#/components/schemas/AgentCardData'
            - $ref: '#/components/schemas/GroupMutationData'
            - description: Other integration-defined JSON data.
    UnknownPart:
      type: object
      additionalProperties: true
      required:
        - part_index
        - type
      description: >
        The catch-all that keeps `Part` open. Anything whose `type` is not one
        of the published kinds matches here and still carries its identity.
      properties:
        part_index:
          type: integer
          minimum: 0
        part_id:
          $ref: '#/components/schemas/PartId'
        position:
          type: integer
          minimum: 0
          description: >
            Dense presentation position, always equal to `part_index`.
            `part_index` is retained for shipped clients; neither is ever an
            identity. Reactions and replies key on `part_id`.
        type:
          type: string
          not:
            enum:
              - text
              - media
              - link
              - data
          description: >
            A part kind this version of the spec does not publish. The exclusion
            is what keeps the union unambiguous: a known kind is always checked
            against its own shape and never falls through here.
    Mention:
      type: string
      pattern: ^[a-z][a-z0-9_]{2,31}(\.[a-z][a-z0-9_]{2,31})?$
      description: >
        The address of a mentioned user or agent, without the leading "@". A
        person, a system agent and a brand's front-door agent carry a flat name;
        every other agent carries its creator's, as name.creator. The text
        `mention_range` covers is display text and does not have to spell it.
    StyleRange:
      type: object
      additionalProperties: false
      required:
        - start
        - length
        - styles
      properties:
        start:
          type: integer
          minimum: 0
          description: UTF-16 code-unit offset of the styled run in `text`.
        length:
          type: integer
          minimum: 1
          description: Length of the styled run in UTF-16 code units.
        styles:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - bold
              - italic
              - underline
              - strikethrough
          description: >
            Formats applied to the run: the four every reference messenger
            ships. The server deduplicates the names and stores them in this
            enum's order, so equal formatting always serializes identically. Any
            other name answers 422.
    AgentCardDataInput:
      type: object
      additionalProperties: false
      required:
        - type
        - handle
      description: >
        The `data` value of a data part that shares a Relay agent. A sender
        names the agent and nothing else: every field the card draws is read
        from the agent record inside the same transaction that commits the
        message, so a card cannot dress one agent in another's identity. Any
        property beyond `type` and `handle` returns 422, as does a handle that
        names no agent or names a retired one.
      properties:
        type:
          type: string
          const: agent_card
        handle:
          type: string
          description: Relay agent handle, without the "@". Lowercased on commit.
          example: relay
    AgentCardData:
      type: object
      additionalProperties: false
      required:
        - type
        - agent
        - fallback
      description: >
        The `data` value recipients receive in place of what was sent. It is a
        snapshot, not a live join: the card keeps saying what was true when it
        was sent, so renaming an agent never rewrites cards already in a
        transcript.
      properties:
        type:
          type: string
          const: agent_card
        agent:
          type: object
          additionalProperties: false
          required:
            - kind
            - id
            - handle
            - display_name
            - tagline
            - verified
          properties:
            kind:
              type: string
              const: agent
            id:
              type: string
              pattern: ^agt_
            handle:
              type: string
            display_name:
              type: string
            tagline:
              type: string
              description: Empty string when the agent has not set one.
            avatar_url:
              type:
                - string
                - 'null'
            accent_color:
              type:
                - string
                - 'null'
            verified:
              type: boolean
              description: >
                True for a first-party Relay agent. Read from the agent record,
                never inferred from the handle.
        fallback:
          type: string
          description: The handle with its "@", drawn wherever no card renderer exists.
          example: '@relay'
    GroupMutationData:
      type: object
      additionalProperties: false
      required:
        - type
        - mutation
        - actor
        - changes
      description: >
        The `data` part of a group notice, carried beside the notice's text so a
        client can render the change itself rather than parsing a sentence.
      properties:
        type:
          type: string
          const: group.mutation
        mutation:
          type: string
          enum:
            - group.created
            - metadata.updated
            - membership.added
            - membership.removed
            - membership.left
        actor:
          $ref: '#/components/schemas/Sender'
        affected_participant:
          allOf:
            - $ref: '#/components/schemas/Sender'
          description: >-
            The person or agent added, removed, or departed. Absent on a
            metadata update.
        changes:
          type: object
          description: Each changed field, as an object with `old` and `new`.
          additionalProperties:
            type: object
            properties:
              old: {}
              new: {}
  responses:
    Unauthorized:
      description: The token or session is absent or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: The resource does not exist or is not visible to this caller.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: The per-conversation message-write ceiling was reached.
      headers:
        Retry-After:
          description: Whole seconds until Relay can admit another new canonical write.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    CommitUnavailable:
      description: >
        `temporarily_unavailable`: the commit was busy, the database was briefly
        unreachable, or the transaction exceeded its time budget. Retry with the
        same `message_id`; idempotency makes that safe.
      headers:
        Retry-After:
          description: Whole seconds before retrying.
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    agentToken:
      type: http
      scheme: bearer
      description: Agent Token (`rly_live_…`), shown once when the agent is created.
    userSession:
      type: http
      scheme: bearer
      description: >-
        Better Auth user-session bearer used by the Relay app and by a paired
        bridge; never an Agent Token.

````