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

# Pull events

> Drain the agent's durable event log past `after`. This is a plain pull: `after` is the last `sequence` you have processed, the page is everything newer, and nothing is acknowledged or consumed. A consumer that sees an event twice ignores it by `event_id`.

`timeout` greater than zero holds the request open until something arrives, up to thirty seconds. Persist the events and the new cursor together before advancing, and read `has_more` to know whether to pull again immediately.

Polling works alongside webhooks: both transports read one log, so an agent may run either or both.




## OpenAPI

````yaml /api-reference/openapi.yaml get /v1/events
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/events:
    get:
      tags:
        - Events
      summary: Pull events
      description: >
        Drain the agent's durable event log past `after`. This is a plain pull:
        `after` is the last `sequence` you have processed, the page is
        everything newer, and nothing is acknowledged or consumed. A consumer
        that sees an event twice ignores it by `event_id`.


        `timeout` greater than zero holds the request open until something
        arrives, up to thirty seconds. Persist the events and the new cursor
        together before advancing, and read `has_more` to know whether to pull
        again immediately.


        Polling works alongside webhooks: both transports read one log, so an
        agent may run either or both.
      operationId: pollEvents
      parameters:
        - name: after
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: >
            The highest `sequence` this consumer has processed. `0` starts from
            the beginning of the retained log. `cursor` is accepted as an alias.
        - name: timeout
          in: query
          schema:
            type: integer
            minimum: 0
            maximum: 30
            default: 0
          description: >-
            Seconds to hold the request open when no events are pending. 0
            returns immediately with any pending events (nonblocking pull).
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 100
      responses:
        '200':
          description: Events past the cursor, oldest first.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - events
                  - next_cursor
                  - latest
                  - has_more
                properties:
                  events:
                    type: array
                    maxItems: 100
                    items:
                      $ref: '#/components/schemas/Event'
                  next_cursor:
                    type: integer
                    description: >
                      The `sequence` of the last event on this page. Pass it as
                      `after` on the next pull; it equals the request's `after`
                      when the page is empty.
                  latest:
                    type: integer
                    description: >-
                      The highest sequence Relay has written to this agent's
                      log.
                  has_more:
                    type: boolean
                    description: >-
                      True when `next_cursor` is behind `latest`, so another
                      page is waiting.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: >
            `after`, `timeout`, or `limit` does not satisfy its exact integer
            bounds.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >
            Too many held-open pulls. Charged only when `timeout` is greater
            than zero.
          headers:
            Retry-After:
              description: Whole seconds before another held-open pull.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Event:
      type: object
      additionalProperties: true
      required:
        - event_id
        - sequence
        - event_type
        - agent_id
        - chat_id
        - created_at
        - schema_version
        - data
      properties:
        event_id:
          type: string
          description: Deduplicate on this; delivery is at least once.
          example: evt_01JZE9M2XW
        sequence:
          type: integer
          minimum: 1
          description: >
            This agent's log position. Pass the highest one you have processed
            as `after` on the next `GET /v1/events`.
        schema_version:
          type: string
          format: date
          example: '2026-08-24'
          description: >
            The envelope shape this delivery was rendered as, repeated inside
            the body so a consumer that queues the envelope still has it an hour
            later, and so it sits inside the signed bytes. Webhooks also receive
            it as the `relay-webhook-version` header.
        event_type:
          $ref: '#/components/schemas/WebhookEventType'
          example: message.received
        agent_id:
          type: string
          example: agt_01JZRELAY
        chat_id:
          type:
            - string
            - 'null'
          description: The conversation this event belongs to, when it belongs to one.
        created_at:
          type: string
          format: date-time
        data:
          description: >
            The shape `event_type` names. Switch on `event_type`, never on which
            branch happens to match.
          oneOf:
            - $ref: '#/components/schemas/MessageEventData'
            - $ref: '#/components/schemas/ReceiptEventData'
            - $ref: '#/components/schemas/RosterEventData'
            - $ref: '#/components/schemas/ChatMetadataEventData'
            - $ref: '#/components/schemas/ReactionEventData'
    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
    WebhookEventType:
      type: string
      enum:
        - message.received
        - message.delivered
        - message.read
        - chat.created
        - chat.group_name_updated
        - chat.group_icon_updated
        - participant.added
        - participant.removed
        - reaction.added
        - reaction.removed
    MessageEventData:
      allOf:
        - $ref: '#/components/schemas/Message'
      description: >-
        The `data` of a `message.received`: the message itself. Read the
        message's fields directly off `data`.
    ReceiptEventData:
      type: object
      additionalProperties: true
      required:
        - message_id
        - chat_id
        - through_sequence
        - at
        - recipient
      description: >
        The `data` of a `message.delivered` or `message.read`: one of this
        agent's messages reached or was read by the other party. Only 1:1
        conversations emit these.
      properties:
        message_id:
          type: string
          pattern: ^msg_
        chat_id:
          type: string
          pattern: ^cnv_
        through_sequence:
          type: integer
          minimum: 1
        at:
          type: string
          format: date-time
        recipient:
          $ref: '#/components/schemas/Sender'
        participant:
          allOf:
            - $ref: '#/components/schemas/Sender'
          description: The same actor as `recipient`, kept for shipped consumers.
    RosterEventData:
      type: object
      additionalProperties: true
      required:
        - chat_id
        - actor
        - affected_participant
        - membership_version
      description: >
        The `data` of `participant.added` and `participant.removed`. On a leave,
        the actor and the affected participant are the same person. The matching
        notice arrives separately as a `message.received` whose message has
        `item_type` 1.
      properties:
        chat_id:
          type: string
          pattern: ^cnv_
        actor:
          $ref: '#/components/schemas/Sender'
        affected_participant:
          allOf:
            - $ref: '#/components/schemas/Sender'
          description: The participant added or removed.
        membership_version:
          type: integer
          minimum: 1
          description: The roster version after the change. It increases by one per change.
    ChatMetadataEventData:
      type: object
      additionalProperties: true
      required:
        - chat_id
        - old_value
        - new_value
        - changed_by_handle
        - updated_at
      description: >
        The `data` of `chat.created`, `chat.group_name_updated` and
        `chat.group_icon_updated`. The matching notice arrives separately as a
        `message.received` whose message has `item_type` 2 (rename) or 3
        (photo).
      properties:
        chat_id:
          type: string
          pattern: ^cnv_
        old_value:
          type:
            - string
            - 'null'
        new_value:
          type:
            - string
            - 'null'
        changed_by_handle:
          $ref: '#/components/schemas/Sender'
        updated_at:
          type: string
          format: date-time
    ReactionEventData:
      allOf:
        - $ref: '#/components/schemas/Reaction'
      description: >-
        The `data` of a `reaction.added` or `reaction.removed`: the reaction
        itself. Read the reaction's fields directly off `data`.
    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
    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
    Reaction:
      type: object
      additionalProperties: false
      required:
        - message_id
        - target_part_id
        - type
        - actor
        - operation
        - changed
      description: >
        One reaction as Relay recorded it. The same object is the reaction
        endpoint's response body and the `data.reaction` of a `reaction.added`
        or `reaction.removed` event, so a consumer reads one shape from both
        paths. It is flatter than the `reactions` array in a projected message,
        which carries `actor_kind` and `actor_id` instead of a nested actor: see
        ProjectedReaction.
      properties:
        message_id:
          type: string
          pattern: ^msg_
        target_part_id:
          oneOf:
            - $ref: '#/components/schemas/PartId'
            - type: 'null'
          description: Null exactly when the reaction is on the whole message.
        type:
          type: string
          enum:
            - love
            - like
            - dislike
            - laugh
            - emphasize
            - question
            - custom
        custom_emoji:
          type: string
          example: ❤️
        actor:
          $ref: '#/components/schemas/Sender'
        operation:
          type: string
          enum:
            - add
            - remove
        changed:
          type: boolean
          description: >
            False when the request was a no-op: removing a reaction that was not
            there, or re-adding the one already in the slot. Events are emitted
            only when this is true, so a retry costs the recipient nothing.
    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
    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.
    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'
  securitySchemes:
    agentToken:
      type: http
      scheme: bearer
      description: Agent Token (`rly_live_…`), shown once when the agent is created.

````