openapi: 3.1.0
info:
  title: Rayachat Developer API
  version: '2026-08-29'
  description: |
    Agent-bound API for sending customer messages through Rayachat, receiving
    grounded assistant replies, reading conversations, and receiving signed
    handoff/manual-reply webhooks.
  contact:
    url: https://rayachat.net
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://rayachat.net/api/v1
security:
  - bearerAuth: []
tags:
  - name: Messages
    description: Send customer messages and receive grounded assistant replies.
  - name: Conversations
    description: Read API-channel conversation state and chronological history.
paths:
  /messages:
    post:
      tags: [Messages]
      operationId: createMessage
      summary: Send a customer message to the configured agent
      description: |
        The API key determines the tenant, agent, and channel. `thread_id` is a
        caller-owned stable thread identity and becomes permanently bound to
        the first `customer.id` used with it. Every request requires a unique
        `Idempotency-Key`; completed responses are replayed for 24 hours.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMessageRequest'
      responses:
        '200':
          description: Completed assistant response.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompletedMessage'
        '202':
          description: The thread is under human control; the inbound message was persisted without an assistant reply or billable message.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HandoffMessage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/GenerationFailed'
  /conversations/{conversation_id}:
    get:
      tags: [Conversations]
      operationId: getConversation
      summary: Get one conversation
      parameters:
        - $ref: '#/components/parameters/ConversationId'
      responses:
        '200':
          description: Conversation metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
  /conversations/{conversation_id}/messages:
    get:
      tags: [Conversations]
      operationId: listConversationMessages
      summary: List conversation messages in chronological order
      parameters:
        - $ref: '#/components/parameters/ConversationId'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: after
          in: query
          description: Message UUID returned as `next_cursor` by the prior page.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Chronological message page.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
webhooks:
  conversationHandoff:
    post:
      operationId: receiveConversationHandoff
      summary: Conversation requires a human operator
      parameters:
        - $ref: '#/components/parameters/WebhookDelivery'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    type:
                      const: conversation.handoff
                    data:
                      $ref: '#/components/schemas/HandoffEventData'
      responses:
        '200':
          description: Any 2xx acknowledges the delivery.
  manualReplyCreated:
    post:
      operationId: receiveManualReplyCreated
      summary: An operator sent a manual reply
      parameters:
        - $ref: '#/components/parameters/WebhookDelivery'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    type:
                      const: message.created
                    data:
                      $ref: '#/components/schemas/ManualReplyEventData'
      responses:
        '200':
          description: Any 2xx acknowledges the delivery.
  webhookTest:
    post:
      operationId: receiveWebhookTest
      summary: Dashboard delivery test
      parameters:
        - $ref: '#/components/parameters/WebhookDelivery'
        - $ref: '#/components/parameters/WebhookEvent'
        - $ref: '#/components/parameters/WebhookTimestamp'
        - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/WebhookEnvelope'
                - type: object
                  properties:
                    type:
                      const: webhook.test
                    data:
                      $ref: '#/components/schemas/WebhookTestEventData'
      responses:
        '200':
          description: Any 2xx acknowledges the delivery.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: rk_live
      description: API key created from the agent's Publish > Developer API panel.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 8–200 visible ASCII characters. Scoped to the API channel and retained for 24 hours.
      schema:
        type: string
        minLength: 8
        maxLength: 200
    ConversationId:
      name: conversation_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    WebhookDelivery:
      name: X-Rayachat-Delivery
      in: header
      required: true
      schema:
        type: string
        format: uuid
    WebhookEvent:
      name: X-Rayachat-Event
      in: header
      required: true
      schema:
        type: string
    WebhookTimestamp:
      name: X-Rayachat-Timestamp
      in: header
      required: true
      schema:
        type: string
        pattern: '^\d+$'
    WebhookSignature:
      name: X-Rayachat-Signature
      in: header
      required: true
      description: '`v1=` followed by HMAC-SHA256 of `<timestamp>.<raw-body>`.'
      schema:
        type: string
        pattern: '^v1=[a-f0-9]{64}$'
  headers:
    RequestId:
      description: Correlation id for support and logs.
      schema:
        type: string
    IdempotentReplayed:
      description: Present and `true` when a stored response was replayed.
      schema:
        type: string
        enum: ['true']
  responses:
    BadRequest:
      description: Invalid JSON, schema, cursor, or idempotency key.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Missing, malformed, unknown, expired, or revoked API key.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: Suspended account, disabled channel, missing scope, blocked customer, or inactive subscription.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: Idempotency conflict/in-progress request or thread/customer mismatch.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: The resource is outside this key's tenant/channel scope or does not exist.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    RateLimited:
      description: Per-key, customer, tenant, daily, quota, or wallet limit reached.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    GenerationFailed:
      description: The model/provider failed after admission. Use a new Idempotency-Key to retry.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    CreateMessageRequest:
      type: object
      additionalProperties: false
      required: [thread_id, customer, message]
      properties:
        thread_id:
          type: string
          minLength: 1
          maxLength: 200
          example: order-1001
        customer:
          type: object
          additionalProperties: false
          required: [id]
          properties:
            id:
              type: string
              minLength: 1
              maxLength: 200
              example: customer-42
            name:
              type: string
              maxLength: 120
            username:
              type: string
              maxLength: 120
        message:
          type: string
          minLength: 1
          maxLength: 5000
    Citation:
      type: object
      required: [chunkId, documentId, score]
      properties:
        chunkId:
          type: string
          format: uuid
        documentId:
          type: string
          format: uuid
        score:
          type: number
        type:
          type: string
          enum: [kb, correction]
    CompletedMessage:
      type: object
      required:
        [
          object,
          status,
          message_id,
          conversation_id,
          thread_id,
          response,
          usage,
          latency_ms,
          request_id,
        ]
      properties:
        object:
          const: chat.message
        status:
          const: completed
        message_id:
          type: string
          format: uuid
        conversation_id:
          type: string
          format: uuid
        thread_id:
          type: string
        response:
          type: object
          required: [role, content, citations]
          properties:
            role:
              const: assistant
            content:
              type: string
            citations:
              type: array
              items:
                $ref: '#/components/schemas/Citation'
        usage:
          $ref: '#/components/schemas/Usage'
        latency_ms:
          type: integer
        request_id:
          type: string
    HandoffMessage:
      type: object
      required: [object, status, conversation_id, thread_id, request_id]
      properties:
        object:
          const: chat.message
        status:
          const: handoff
        message_id:
          type: 'null'
        conversation_id:
          type: string
          format: uuid
        thread_id:
          type: string
        response:
          type: 'null'
        request_id:
          type: string
    Usage:
      type: object
      required: [input_tokens, output_tokens]
      properties:
        input_tokens:
          type: integer
        output_tokens:
          type: integer
    Conversation:
      type: object
      required:
        [
          object,
          id,
          thread_id,
          customer,
          status,
          automation,
          needs_attention,
          message_count,
          created_at,
          last_message_at,
        ]
      properties:
        object:
          const: conversation
        id:
          type: string
          format: uuid
        thread_id:
          type: string
        customer:
          type: object
          properties:
            id:
              type: string
            name:
              type: [string, 'null']
            username:
              type: [string, 'null']
        status:
          type: string
          enum: [open, waiting, resolved]
        automation:
          type: string
          enum: [active, paused]
        needs_attention:
          type: boolean
        attention_reason:
          type: [string, 'null']
        message_count:
          type: integer
        created_at:
          type: string
          format: date-time
        last_message_at:
          type: string
          format: date-time
        request_id:
          type: string
    Message:
      type: object
      required: [object, id, role, content, created_at]
      properties:
        object:
          const: message
        id:
          type: string
          format: uuid
        role:
          type: string
          enum: [user, assistant]
        content:
          type: string
        citations:
          type: [array, 'null']
          items:
            $ref: '#/components/schemas/Citation'
        model:
          type: [string, 'null']
        usage:
          oneOf:
            - $ref: '#/components/schemas/Usage'
            - type: 'null'
        created_at:
          type: string
          format: date-time
    MessageList:
      type: object
      required: [object, conversation_id, data, has_more, next_cursor, request_id]
      properties:
        object:
          const: list
        conversation_id:
          type: string
          format: uuid
        data:
          type: array
          items:
            $ref: '#/components/schemas/Message'
        has_more:
          type: boolean
        next_cursor:
          type: [string, 'null']
          format: uuid
        request_id:
          type: string
    Problem:
      type: object
      required: [type, title, status, code, detail, request_id]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        code:
          type: string
        detail:
          type: string
        request_id:
          type: string
        retry_after:
          type: integer
    WebhookEnvelope:
      type: object
      required: [id, object, api_version, created_at, type, data]
      properties:
        id:
          type: string
          format: uuid
        object:
          const: event
        api_version:
          const: '2026-08-29'
        created_at:
          type: string
          format: date-time
        type:
          type: string
        data:
          type: object
    HandoffEventData:
      type: object
      required: [conversation_id, thread_id, reason, customer, occurred_at]
      properties:
        conversation_id:
          type: string
          format: uuid
        thread_id:
          type: string
        reason:
          type: string
          enum:
            - customer_waiting
            - bot_paused
            - ai_failed
            - ai_requested
            - ai_customer_requested
            - ai_sensitive_issue
            - ai_unsupported_action
            - ai_payment_or_refund
            - ai_low_confidence
        customer:
          type: object
          properties:
            id:
              type: string
            name:
              type: [string, 'null']
            username:
              type: [string, 'null']
        occurred_at:
          type: string
          format: date-time
    ManualReplyEventData:
      type: object
      required: [conversation_id, thread_id, message]
      properties:
        conversation_id:
          type: string
          format: uuid
        thread_id:
          type: string
        message:
          type: object
          required: [id, role, content, created_at]
          properties:
            id:
              type: string
              format: uuid
            role:
              const: assistant
            content:
              type: string
            created_at:
              type: string
              format: date-time
    WebhookTestEventData:
      type: object
      required: [message, sent_at]
      properties:
        message:
          const: Rayachat webhook test
        sent_at:
          type: string
          format: date-time
