openapi: 3.0.3
info:
  title: Liya AI — External API
  description: |
    REST API for integrating Liya AI into your own applications.

    Use this API to build chat widgets, 3D avatar integrations, voice assistants,
    or any custom AI-powered experience on top of the Liya AI platform.

    ## Authentication

    Every request to `/api/v1/external/` must include your API key in the
    `X-API-Key` header. Get your key from
    [ai.liyalabs.com](https://ai.liyalabs.com) → **Projects → API Keys**.

    ```
    X-API-Key: your-api-key
    ```

    ## GAR — Global Application Router

    Liya AI uses a distributed backend. Each project is routed to a dedicated
    backend instance. Your backend URL is shown in the dashboard under
    **Settings → API Configuration**:

    ```
    https://app-{X}-ai.liyalabs.com
    ```

    Always use your assigned instance URL as the base URL for all requests.

    ## Rate Limiting

    - 120 requests / minute per API key
    - Remaining quota: `X-RateLimit-Remaining` response header
    - Reset time (Unix): `X-RateLimit-Reset` response header

    ## Account Tiers & Feature Access

    | Feature | STANDARD | PREMIUM | PREMIUM PLUS |
    |---------|----------|---------|--------------|
    | Chat | ✅ | ✅ | ✅ |
    | File Upload | ❌ | ✅ | ✅ |
    | Voice Output (TTS) | ❌ | ✅ | ✅ |
    | 3D Avatar | ❌ | ✅ | ✅ |
    | Custom Avatar Model | ❌ | ❌ | ✅ |
    | Kiosk Mode | ❌ | ❌ | ✅ |

    ## Stability & Versioning

    This API follows [Semantic Versioning](https://semver.org).

    - **Breaking changes** bump the major version (v1 → v2)
      and are announced **60 days in advance**.
    - **New features** bump the minor version (v0.1 → v0.2)
      and are backwards-compatible.
    - **Bug fixes** bump the patch version (v0.1.0 → v0.1.1).

    While the version is **0.x**, breaking changes may occur without prior notice.
    Subscribe to the changelog at [liyalabs.com/changelog](https://liyalabs.com/changelog)
    to stay informed.

  version: 0.1.0
  contact:
    name: Liya Labs Support
    url: https://liyalabs.com
    email: support@liyalabs.com
  license:
    name: Proprietary
    url: https://liyalabs.com/terms

servers:
  - url: https://app-1-ai.liyalabs.com
    description: Instance 1
  - url: https://app-2-ai.liyalabs.com
    description: Instance 2
  - url: https://app-3-ai.liyalabs.com
    description: Instance 3

security:
  - ApiKeyAuth: []

tags:
  - name: Assistants
    description: List the AI assistants available in your project
  - name: Sessions
    description: Create and manage conversation sessions per user
  - name: Chat
    description: Send messages and receive AI responses
  - name: Files
    description: Upload files to attach to chat messages
  - name: Avatar & TTS
    description: Text-to-speech and 3D avatar lip-sync (PREMIUM+)
  - name: Tasks
    description: Poll async task status (image / video generation)
  - name: Translation
    description: Translate text between 100+ languages

paths:
  # ─── ASSISTANTS ────────────────────────────────────────────────────────────

  /api/v1/external/assistants/:
    get:
      tags: [Assistants]
      summary: List assistants
      description: Returns all assistants that belong to your project.
      parameters:
        - name: type
          in: query
          description: Filter by assistant type
          required: false
          schema:
            type: string
            enum: [custom, coding_agent, linux_os]
      responses:
        "200":
          description: List of assistants
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Assistant"
              example:
                status: success
                data:
                  - id: "550e8400-e29b-41d4-a716-446655440000"
                    name: "Customer Support"
                    model: "gpt-4o"
                    assistant_type: "custom"
                    is_reasoning_model: false
                    total_messages: 1240
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── SESSIONS ──────────────────────────────────────────────────────────────

  /api/v1/external/sessions/:
    get:
      tags: [Sessions]
      summary: List sessions
      description: |
        Returns sessions for a given assistant. Use `external_session_id` to map
        sessions to your own user IDs.
      parameters:
        - name: assistant_id
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Paginated session list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/SessionListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      tags: [Sessions]
      summary: Create session
      description: |
        Creates a new conversation session. Pass an `external_session_id` to
        associate the session with a user in your own system.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionRequest"
            example:
              assistant_id: "550e8400-e29b-41d4-a716-446655440000"
              session_name: "Support ticket #1234"
              external_session_id: "user_42"
      responses:
        "201":
          description: Created session
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/Session"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/v1/external/sessions/{session_id}/:
    parameters:
      - name: session_id
        in: path
        required: true
        schema:
          type: string
          format: uuid

    get:
      tags: [Sessions]
      summary: Get session
      responses:
        "200":
          description: Session detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/Session"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      tags: [Sessions]
      summary: Close session
      description: Soft-deletes the session. History is preserved but the session becomes inactive.
      responses:
        "200":
          description: Session closed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  message:
                    type: string
                    example: Session closed
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/v1/external/sessions/{session_id}/history/:
    get:
      tags: [Sessions]
      summary: Get chat history
      description: Returns all messages in a session, paginated.
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Message history
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/SessionHistoryResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── CHAT ──────────────────────────────────────────────────────────────────

  /api/v1/external/chat/:
    post:
      tags: [Chat]
      summary: Send message
      description: |
        Sends a message to an assistant and returns the AI response.

        - If `session_id` is omitted, a new session is created automatically.
        - The `media` array in the response contains images or videos extracted
          from the AI response by the MediaExtractor (e.g. from DALL-E or Kling AI).
        - If the response includes an `async_task_id` in `metadata`, the generation
          is still in progress — poll `/tasks/{task_id}/status/` for the result.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendMessageRequest"
            example:
              assistant_id: "550e8400-e29b-41d4-a716-446655440000"
              message: "Hello, how can you help me?"
              session_id: "660e8400-e29b-41d4-a716-446655440000"
      responses:
        "200":
          description: Assistant response
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/ChatResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientBalance"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/external/chat/with-files/:
    post:
      tags: [Chat]
      summary: Send message with files
      description: |
        Send a message referencing previously uploaded files.
        Upload files first via `POST /files/` and pass the returned IDs here.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/SendMessageRequest"
                - type: object
                  properties:
                    file_ids:
                      type: array
                      items:
                        type: string
                        format: uuid
                      description: File IDs returned by the upload endpoint
            example:
              assistant_id: "550e8400-e29b-41d4-a716-446655440000"
              message: "Analyze this document"
              session_id: "660e8400-e29b-41d4-a716-446655440000"
              file_ids: ["770e8400-e29b-41d4-a716-446655440000"]
      responses:
        "200":
          description: Assistant response
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/ChatResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientBalance"

  # ─── FILES ─────────────────────────────────────────────────────────────────

  /api/v1/external/files/:
    post:
      tags: [Files]
      summary: Upload file
      description: |
        Uploads a file and attaches it to a session. Maximum 50 MB.

        Supported formats: `txt`, `csv`, `md`, `pdf`, `doc`, `docx`, `xls`,
        `xlsx`, `json`, and all common image formats.

        The returned `id` can be passed as a `file_id` in subsequent chat requests.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [session_id, file]
              properties:
                session_id:
                  type: string
                  format: uuid
                file:
                  type: string
                  format: binary
      responses:
        "201":
          description: Uploaded file info
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/FileAttachment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── AVATAR & TTS ──────────────────────────────────────────────────────────

  /api/v1/external/tts/:
    post:
      tags: [Avatar & TTS]
      summary: Text-to-speech
      description: |
        Converts text to audio. Returns the audio file directly as a binary blob
        (not JSON). Use this for simple voice output without lip-sync.

        **Requirement:** PREMIUM or higher account.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TTSRequest"
      responses:
        "200":
          description: Audio file (Opus or MP3)
          content:
            audio/ogg:
              schema:
                type: string
                format: binary
            audio/mpeg:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/external/avatar/speech/:
    post:
      tags: [Avatar & TTS]
      summary: Avatar speech (visemes + audio)
      description: |
        Converts text to speech and returns ARKit-compatible viseme timing data
        for real-time 3D avatar lip-sync.

        Audio is returned as Base64 in the JSON response body.
        On iOS/Safari the format is automatically set to MP3; all other
        browsers receive Opus.

        **Requirement:** PREMIUM or higher account.

        ### Viseme morph targets
        The `visemes` array drives these blend shape targets on your avatar model:
        `viseme_PP`, `viseme_FF`, `viseme_TH`, `viseme_DD`, `viseme_kk`,
        `viseme_CH`, `viseme_SS`, `viseme_nn`, `viseme_RR`, `viseme_aa`,
        `viseme_E`, `viseme_I`, `viseme_O`, `viseme_U`
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AvatarSpeechRequest"
            example:
              text: "Hello! How can I help you today?"
              voice: nova
              speed: 1.0
              include_audio: true
      responses:
        "200":
          description: Viseme data and Base64 audio
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/AvatarSpeechResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/external/avatar/model/:
    get:
      tags: [Avatar & TTS]
      summary: Get avatar model URL
      description: |
        Returns the HTTPS URL of the GLB/GLTF avatar model assigned to an assistant.

        - PREMIUM accounts get the default Liya avatar model.
        - PREMIUM PLUS accounts may have a custom model assigned per assistant.

        **Requirement:** PREMIUM or higher account.
      parameters:
        - name: assistant_id
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Avatar model info
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/AvatarModelResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/v1/external/user/access/:
    get:
      tags: [Avatar & TTS]
      summary: Check feature access
      description: |
        Returns the account tier and which premium features are available.
        Use this on widget load to conditionally show the avatar button,
        voice button, or file upload.
      responses:
        "200":
          description: Access info
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/UserAccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ─── TASKS ─────────────────────────────────────────────────────────────────

  /api/v1/external/tasks/{task_id}/status/:
    get:
      tags: [Tasks]
      summary: Get async task status
      description: |
        Polls the status of an asynchronous task such as image or video generation
        (Kling AI).

        When a chat response contains `metadata.async_task_id`, the media is still
        being generated. Poll this endpoint until `status` is `completed` or
        `failed`, then use the `result_urls` to display the output.

        **Recommended polling interval:** 3–5 seconds.
      parameters:
        - name: task_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Task status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  data:
                    $ref: "#/components/schemas/TaskStatusResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

# ─── COMPONENTS ──────────────────────────────────────────────────────────────

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Your Liya AI API key, obtained from the dashboard

  schemas:
    # ── Core objects ──────────────────────────────────────────────────────────

    Assistant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: Customer Support
        description:
          type: string
        model:
          type: string
          example: gpt-4o
        model_display:
          type: string
          example: GPT-4o
        assistant_type:
          type: string
          enum: [custom, coding_agent, linux_os]
          example: custom
        is_reasoning_model:
          type: boolean
        total_messages:
          type: integer

    Session:
      type: object
      properties:
        id:
          type: string
          format: uuid
        assistant_id:
          type: string
          format: uuid
        session_name:
          type: string
          example: New Conversation
        message_count:
          type: integer
        created_at:
          type: string
          format: date-time
        last_message_at:
          type: string
          format: date-time
          nullable: true
        external_session_id:
          type: string
          nullable: true

    SessionListResponse:
      type: object
      properties:
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/Session"
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer

    SessionHistoryResponse:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
        messages:
          type: array
          items:
            $ref: "#/components/schemas/Message"
        total:
          type: integer

    Message:
      type: object
      properties:
        id:
          type: string
          format: uuid
        content:
          type: string
          description: Message text (markdown supported for assistant messages)
        role:
          type: string
          enum: [user, assistant]
        created_at:
          type: string
          format: date-time
        response_time:
          type: number
          format: float
          description: Assistant response time in seconds
          nullable: true

    MessageMediaItem:
      type: object
      description: Image or video extracted from an assistant response
      properties:
        type:
          type: string
          enum: [image, video]
        url:
          type: string
          format: uri
        alt:
          type: string
          nullable: true
        source:
          type: string
          description: Content source identifier (e.g. dall-e, klingai)
          nullable: true

    FileAttachment:
      type: object
      properties:
        id:
          type: string
          format: uuid
        file_name:
          type: string
        file_size:
          type: integer
          description: File size in bytes
        file_type:
          type: string
        openai_file_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    # ── Requests ──────────────────────────────────────────────────────────────

    CreateSessionRequest:
      type: object
      required: [assistant_id]
      properties:
        assistant_id:
          type: string
          format: uuid
        session_name:
          type: string
          example: Support ticket #1234
        external_session_id:
          type: string
          description: Your own user/session identifier for mapping

    SendMessageRequest:
      type: object
      required: [assistant_id, message]
      properties:
        assistant_id:
          type: string
          format: uuid
        message:
          type: string
          example: What are your business hours?
        session_id:
          type: string
          format: uuid
          description: Omit to create a new session automatically
        external_session_id:
          type: string
          description: Your own identifier for this session

    TTSRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          maxLength: 4096
          example: Hello, how can I help you today?
        voice:
          type: string
          default: nova
          description: OpenAI TTS voice name
          example: nova
        speed:
          type: number
          format: float
          default: 1.0
          minimum: 0.25
          maximum: 4.0

    AvatarSpeechRequest:
      type: object
      required: [text]
      properties:
        text:
          type: string
          maxLength: 4096
        voice:
          type: string
          default: nova
        speed:
          type: number
          format: float
          default: 1.0
        include_audio:
          type: boolean
          default: true
          description: Set false to return only viseme data without audio

    # ── Responses ─────────────────────────────────────────────────────────────

    ChatResponse:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
        message_id:
          type: string
          format: uuid
        response:
          type: string
          description: Assistant reply text (markdown supported)
        response_time:
          type: number
          format: float
          description: Time to generate response in seconds
        suggestions:
          type: array
          items:
            type: string
          description: Quick-reply suggestions to display to the user
        metadata:
          type: object
          nullable: true
          properties:
            async_task_id:
              type: string
              format: uuid
              description: Present when an async task (image/video gen) was started
            async_task_type:
              type: string
              example: klingai
        annotations:
          type: array
          description: Web search citations (if web search was used)
          items:
            type: object
            properties:
              type:
                type: string
                enum: [url_citation, file_citation]
              url:
                type: string
                format: uri
              title:
                type: string
        media:
          type: array
          description: Images/videos extracted from the response (MediaExtractor)
          items:
            $ref: "#/components/schemas/MessageMediaItem"
        usage:
          type: object
          nullable: true
          properties:
            tokens_used:
              type: integer
            cost_usd:
              type: number
              format: float

    VisemeData:
      type: object
      description: Single viseme timing entry for lip-sync animation
      properties:
        time:
          type: number
          format: float
          description: Timestamp in seconds
        viseme:
          type: integer
          description: Viseme index (0–20, ARKit compatible)
        duration:
          type: number
          format: float
          description: Duration in seconds

    AvatarSpeechResponse:
      type: object
      properties:
        visemes:
          type: array
          items:
            $ref: "#/components/schemas/VisemeData"
        duration:
          type: number
          format: float
          description: Total audio duration in seconds
        text:
          type: string
        audio_base64:
          type: string
          format: byte
          description: Base64-encoded audio data (present when include_audio=true)
        audio_format:
          type: string
          enum: [mp3, opus]
          description: mp3 on iOS/Safari, opus on all other browsers
        audio_mime:
          type: string
          example: audio/mpeg

    AvatarModelResponse:
      type: object
      properties:
        model_url:
          type: string
          format: uri
          description: HTTPS URL of the GLB/GLTF avatar model file
        model_type:
          type: string
          example: glb
        source:
          type: string
          enum: [default, custom]
          description: default = Liya default model, custom = assistant-specific model

    UserAccessResponse:
      type: object
      properties:
        has_avatar_access:
          type: boolean
          description: True for PREMIUM and above
        account_type:
          type: string
          enum: [standard, premium, premium_plus, enterprise, system_admin]
        can_use_custom_avatar:
          type: boolean
          description: True for PREMIUM_PLUS and above

    TaskStatusResponse:
      type: object
      properties:
        task_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, submitted, processing, completed, failed, cancelled]
        task_type:
          type: string
          example: klingai
        result_urls:
          type: array
          items:
            type: string
            format: uri
          description: Output URLs when status is completed
        error_code:
          type: string
          nullable: true
        error_message:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true

    TranslationRequest:
      type: object
      required: [text, target_language]
      properties:
        text:
          type: string
          maxLength: 10000
          description: Text to translate
          example: "Hello, how are you?"
        target_language:
          type: string
          description: Target language code (e.g. tr, en, zh, de, fr)
          example: "tr"
        source_language:
          type: string
          description: Source language code (auto-detected if omitted)
          example: "en"

    TranslationResponse:
      type: object
      properties:
        translated_text:
          type: string
          description: Translated text
          example: "Merhaba, nasılsın?"
        source_language:
          type: string
          description: Detected or provided source language code
          example: "en"
        target_language:
          type: string
          description: Target language code
          example: "tr"

    LanguageInfo:
      type: object
      properties:
        code:
          type: string
          description: ISO 639-1 language code
          example: "en"
        name:
          type: string
          description: Language name in English
          example: "English"
        native_name:
          type: string
          description: Language name in its own script
          example: "English"

    ErrorResponse:
      type: object
      properties:
        status:
          type: string
          example: error
        code:
          type: string
          example: INVALID_API_KEY
        message:
          type: string
          example: The API key provided is invalid or expired.
        data:
          type: object
          nullable: true

  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: INVALID_API_KEY
            message: The API key provided is invalid or expired.

    Forbidden:
      description: Feature not available on this account tier
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: FEATURE_NOT_AVAILABLE
            message: 3D Avatar features require Premium or higher subscription.

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: NOT_FOUND
            message: The requested resource was not found.

    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: BAD_REQUEST
            message: assistant_id is required.

    InsufficientBalance:
      description: Insufficient account balance
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: INSUFFICIENT_BALANCE
            message: Insufficient balance to process this request.
            data:
              current_balance: 0.50
              estimated_cost: 0.003

    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Requests remaining in the current window
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp when the rate limit resets
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            status: error
            code: RATE_LIMITED
            message: Rate limit exceeded. Please wait before retrying.
