openapi: 3.1.0
info:
  title: Casola API
  version: "1"
  description: Real-time avatar streaming, offline avatar video, and video
    generation — one API key.
  contact:
    email: support@casola.ai
servers:
  - url: https://api.casola.ai
    description: Production
  - url: https://api.casola-staging.net
    description: Staging
  - url: http://localhost:8787
    description: Local dev
tags:
  - name: Workspaces
    description: Workspace management
  - name: Tokens
    description: API key management
  - name: Sessions
    description: Live avatar sessions
  - name: Avatars
    description: Avatar management and versions
  - name: Voices
    description: Voice management and versions
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "API key issued from the dashboard. Format: `avatar_<8hex>.<56hex>`."
  schemas:
    Workspace:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        plan:
          type: string
        owner_user_id:
          type: string
          format: uuid
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
        updated_at:
          type:
            - integer
            - "null"
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - name
        - plan
        - owner_user_id
        - created_at
        - updated_at
    WorkspaceList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Workspace"
      required:
        - data
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    CreateWorkspaceBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        plan:
          type: string
          default: free
      required:
        - name
    UpdateWorkspaceBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
      required:
        - name
    Token:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        token_prefix:
          type: string
          description: First 8 characters of the raw token (for display)
          example: avatar_a
        scopes:
          anyOf:
            - type: array
              items:
                type: string
            - type: string
        status:
          type: string
          enum:
            - active
            - revoked
        workspace_id:
          type:
            - string
            - "null"
          format: uuid
        kind:
          type: string
          description: 'Token kind: "secret" or "publishable".'
        allowed_origins:
          type:
            - string
            - "null"
          description: JSON-encoded array of allowed origins for publishable keys (raw D1
            string, like `scopes`); null for secret keys.
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - name
        - token_prefix
        - scopes
        - status
        - workspace_id
        - kind
        - allowed_origins
        - created_at
    TokenList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Token"
      required:
        - data
    CreatedToken:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        token:
          type: string
          description: "Full token value — only returned on creation. Format:
            `avatar_<8hex>.<56hex>` (secret) or `pk_<8hex>.<56hex>`
            (publishable)."
          example: avatar_abc12345.def6789012345678901234567890123456789012345678901234
        scopes:
          type: array
          items:
            type: string
        kind:
          type: string
          description: 'Token kind: "secret" or "publishable".'
        allowed_origins:
          type:
            - array
            - "null"
          items:
            type: string
          description: Allowed origins for publishable keys; null for secret keys.
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - name
        - token
        - scopes
        - kind
        - allowed_origins
        - created_at
    CreateTokenBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        kind:
          type: string
          enum:
            - secret
            - publishable
          description: Token kind. Defaults to "secret".
        scopes:
          type: array
          items:
            type: string
          default:
            - "*"
          description: Ignored for publishable keys — always locked to ["session:connect"].
        allowedOrigins:
          type: array
          items:
            type: string
            format: uri
          description: Required for publishable keys — at least one http(s) origin allowed
            to use the key.
        workspaceId:
          type: string
          format: uuid
      required:
        - name
    RevokeTokenResponse:
      type: object
      properties:
        ok:
          type: boolean
          enum:
            - true
        revoked_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - ok
        - revoked_at
    Device:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
        name:
          type: string
        platform:
          type: string
          enum:
            - ios
            - android
            - other
        status:
          type: string
          enum:
            - active
            - revoked
        created_at:
          type: integer
        last_seen_at:
          type:
            - integer
            - "null"
        revoked_at:
          type:
            - integer
            - "null"
      required:
        - id
        - workspace_id
        - name
        - platform
        - status
        - created_at
    DeviceList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Device"
      required:
        - data
    CreatedDevice:
      type: object
      properties:
        device:
          $ref: "#/components/schemas/Device"
        token:
          type: string
          description: "Device credential — only returned on creation/rotation.
            Scope-locked to session:connect. Format: `dt_<8hex>.<56hex>`."
          example: dt_abc12.def6789012345678901234567890123456789012345678901234567890
      required:
        - device
        - token
    CreateDeviceBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        platform:
          type: string
          enum:
            - ios
            - android
            - other
          default: other
      required:
        - name
    RotatedDeviceToken:
      type: object
      properties:
        token:
          type: string
        rotated_at:
          type: integer
      required:
        - token
        - rotated_at
    TrustedIssuer:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
        iss:
          type: string
          format: uri
        aud:
          type: string
        jwks_uri:
          type: string
          format: uri
        algs:
          anyOf:
            - type: array
              items:
                type: string
                enum:
                  - RS256
                  - ES256
            - type: string
        created_at:
          type: integer
      required:
        - id
        - workspace_id
        - iss
        - aud
        - jwks_uri
        - algs
        - created_at
    IssuerList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/TrustedIssuer"
      required:
        - data
    CreateIssuerBody:
      type: object
      properties:
        iss:
          type: string
          format: uri
          example: https://auth.example.com
        aud:
          type: string
          description: Client/audience id of the native app
        jwks_uri:
          type: string
          format: uri
          example: https://auth.example.com/.well-known/jwks
        algs:
          type: array
          items:
            type: string
            enum:
              - RS256
              - ES256
          default:
            - RS256
      required:
        - iss
        - aud
        - jwks_uri
    CreatedSession:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
        connect_url:
          type: string
          description: WebSocket URL to connect to the avatar edge.
        session_token:
          type: string
          description: Short-lived JWT to authenticate the WebSocket connection. Valid for
            60 seconds.
        expires_at:
          type: integer
          description: Unix timestamp (seconds) when the session token expires.
      required:
        - session_id
        - connect_url
        - session_token
        - expires_at
    CreateSessionBody:
      type: object
      properties:
        workspace_id:
          type: string
          format: uuid
          description: Workspace to create the session under. Required if the token is not
            workspace-scoped.
        avatar_ref:
          type: string
          format: uuid
          description: Avatar version ID to use for this session.
        voice_ref:
          type: string
          format: uuid
          description: Voice version ID to use for this session.
        domain_allowlist:
          type: array
          items:
            type: string
          description: Domains allowed to connect to this session. Empty = all.
        concurrency_cap:
          type: integer
        metadata:
          type: object
          additionalProperties: {}
        agent:
          type: object
          additionalProperties: {}
          description: "Opaque tenant agent-callback descriptor, embedded verbatim as the
            `agent` claim of the session JWT (max 1 KB serialized). The box
            presents it back to your endpoint for turn capture, session-start
            context, and tool calls. Suggested shape: { v, hook (URL), tok
            (tenant-signed bearer), conv (conversation id), tools[] }."
    SeatLifecycleBody:
      type: object
      properties:
        seat_token:
          type: string
          description: Per-placement capability from the mint response. When present it is
            the sole credential (native clients scope-locked to session:connect
            use this); when absent the call requires a sessions:write bearer +
            workspace membership.
    Session:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        api_token_id:
          type:
            - string
            - "null"
          format: uuid
        avatar_ref:
          type:
            - string
            - "null"
          format: uuid
        voice_ref:
          type:
            - string
            - "null"
          format: uuid
        status:
          type: string
          description: "Session lifecycle status: pending, active, ended."
        session_token_id:
          type:
            - string
            - "null"
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - workspace_id
        - api_token_id
        - avatar_ref
        - voice_ref
        - status
        - session_token_id
        - created_at
    SpeakContentPart:
      type: object
      properties:
        type:
          type: string
          enum:
            - image_url
            - audio_url
            - input_audio
        image_url:
          type: object
          properties:
            url:
              type: string
            detail:
              type: string
          required:
            - url
        audio_url:
          type: object
          properties:
            url:
              type: string
          required:
            - url
        input_audio:
          type: object
          properties:
            data:
              type: string
            format:
              type: string
          required:
            - data
      required:
        - type
    SpeakBody:
      type: object
      properties:
        text:
          type: string
          description: The words the avatar should say.
        workspace_id:
          type: string
          description: Workspace to attribute the generation to. Required if the token is
            not workspace-scoped.
        content:
          type: array
          items:
            $ref: "#/components/schemas/SpeakContentPart"
          description: OpenAI-style content parts (image_url + audio_url / input_audio).
        image:
          type: string
          description: Face image as an http(s) URL or data URI (flat alternative to
            content).
        voice_ref:
          type: string
          description: Voice reference to clone, as an http(s) URL or data URI.
        voice_text:
          type: string
          description: Transcript of voice_ref. Omit to auto-transcribe (Whisper) + cache.
        immutable_url_for_caching:
          type: boolean
          description: Treat media URLs as immutable cache keys (skip re-download).
            Default false.
        end_frame:
          type: string
          description: Optional last-frame face; clip ends on this pose.
        prompt:
          type: string
          description: Scene/style prompt for the video model.
        num_step:
          type: integer
          description: TTS denoising steps. Default 16.
        guidance_scale:
          type: number
          description: TTS guidance. Default 2.0.
        first_frame_strength:
          type: number
          description: First-frame identity pin. Default 0.5.
        last_frame_strength:
          type: number
          description: Last-frame pin. Default 0.0.
        width:
          type: integer
          description: Output width (px). Default 352.
        height:
          type: integer
          description: Output height (px). Default 480.
        frame_rate:
          type: integer
          description: Output fps. Default 24.
      required:
        - text
    PrebuildResult:
      type: object
      properties:
        ok:
          type: boolean
          description: True when at least one box accepted (or already had) the warm.
        avatar_ref:
          type: string
        boxes:
          type: array
          items:
            type: object
            properties:
              box_id:
                type: string
              status:
                type: string
                description: "'building' | 'cached' | 'prebuild_unsupported' | 'unreachable' |
                  'error_<code>'"
            required:
              - box_id
              - status
      required:
        - ok
        - avatar_ref
        - boxes
    PrebuildBody:
      type: object
      properties:
        avatar_ref:
          type: string
          description: The avatar VERSION id to warm (the same ref sessions are minted
            with).
      required:
        - avatar_ref
    DesignVoiceResult:
      type: object
      properties:
        audio_base64:
          type: string
          description: The generated audio as base64-encoded 24 kHz mono WAV (PCM_16).
        sample_rate:
          type: integer
          description: Sample rate of the audio (Hz).
          example: 24000
        duration_seconds:
          type: number
          description: Duration of the generated clip (seconds).
        gen_ms:
          type: number
          description: Server-side generation time (milliseconds).
        rtf:
          type:
            - number
            - "null"
          description: Real-time factor (gen_time / audio_duration).
        format:
          type: string
          description: Audio container format.
          example: wav
      required:
        - audio_base64
        - sample_rate
        - duration_seconds
        - gen_ms
        - rtf
        - format
    DesignVoiceBody:
      type: object
      properties:
        description:
          type: string
          description: Free-form natural-language description of the voice — timbre,
            emotion, character, pacing. e.g. "an old wise wizard, deep gravelly
            voice, slow and deliberate".
          example: a calm warm female narrator, gentle and clear
        text:
          type: string
          description: The text the designed voice should speak.
          example: Hello there, this is a preview of your new voice.
        temperature:
          type: number
          description: Sampling temperature (default 1.5).
        top_p:
          type: number
          description: Nucleus sampling top-p (default 0.6).
        top_k:
          type: integer
          description: Top-k sampling (default 50).
        repetition_penalty:
          type: number
          description: Repetition penalty (default 1.1).
        max_new_tokens:
          type: integer
          description: Decode length cap (default 4096).
      required:
        - description
        - text
    Avatar:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        name:
          type: string
        kind:
          type: string
          description: Avatar kind (e.g. "stock").
        current_version_id:
          type:
            - string
            - "null"
          format: uuid
        status:
          type: string
        deleted_at:
          type:
            - integer
            - "null"
        region_tag:
          type: string
          description: Preferred edge region (e.g. "us").
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
        updated_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - workspace_id
        - name
        - kind
        - current_version_id
        - status
        - deleted_at
        - region_tag
        - created_at
        - updated_at
    AvatarList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Avatar"
      required:
        - data
    CreateAvatarBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        kind:
          type: string
          description: Avatar kind. Defaults to "stock".
      required:
        - name
    UpdateAvatarBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          description: New display name for the avatar.
      required:
        - name
    AvatarVersion:
      type: object
      properties:
        id:
          type: string
          format: uuid
        avatar_id:
          type: string
          format: uuid
        version_number:
          type: integer
        image_r2_key:
          type:
            - string
            - "null"
          description: Internal R2 key for the avatar image. Non-null once the image is
            uploaded.
        system_prompt:
          type:
            - string
            - "null"
        config_json:
          type:
            - string
            - "null"
          description: JSON-encoded avatar config.
        created_by:
          type:
            - string
            - "null"
          format: uuid
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - avatar_id
        - version_number
        - image_r2_key
        - system_prompt
        - config_json
        - created_by
        - created_at
    AvatarVersionList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AvatarVersion"
      required:
        - data
    CreatedAvatarVersion:
      allOf:
        - $ref: "#/components/schemas/AvatarVersion"
        - type: object
          properties:
            upload_url:
              type: string
              description: Relative URL to PUT the avatar image bytes to.
          required:
            - upload_url
    CreateAvatarVersionBody:
      type: object
      properties:
        system_prompt:
          type: string
        config_json:
          type: object
          additionalProperties: {}
        description:
          type: string
        backstory:
          type: string
        native_language:
          type: string
        voice_ref_text:
          type: string
        inherit_image:
          type: boolean
          description: Carry the current version's face image (and voice ref) forward
            instead of requiring a re-upload — for text-only persona edits.
            Promotes the new version to current when the previous version had an
            image.
    Voice:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workspace_id:
          type: string
          format: uuid
        name:
          type: string
        kind:
          type: string
          description: Voice kind (e.g. "stock", "cloned").
        current_version_id:
          type:
            - string
            - "null"
          format: uuid
        status:
          type: string
        deleted_at:
          type:
            - integer
            - "null"
        region_tag:
          type: string
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
        updated_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - workspace_id
        - name
        - kind
        - current_version_id
        - status
        - deleted_at
        - region_tag
        - created_at
        - updated_at
    VoiceList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Voice"
      required:
        - data
    CreateVoiceBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
        kind:
          type: string
          description: Voice kind. Defaults to "stock".
      required:
        - name
    VoiceVersion:
      type: object
      properties:
        id:
          type: string
          format: uuid
        voice_id:
          type: string
          format: uuid
        version_number:
          type: integer
        sample_r2_key:
          type:
            - string
            - "null"
          description: Internal R2 key for the voice sample audio.
        external_voice_id:
          type:
            - string
            - "null"
          description: Provider voice ID for cloned voices.
        created_by:
          type:
            - string
            - "null"
          format: uuid
        created_at:
          type: integer
          description: Unix timestamp (seconds)
          example: 1718960000
      required:
        - id
        - voice_id
        - version_number
        - sample_r2_key
        - external_voice_id
        - created_by
        - created_at
    VoiceVersionList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/VoiceVersion"
      required:
        - data
    CreatedVoiceVersion:
      allOf:
        - $ref: "#/components/schemas/VoiceVersion"
        - type: object
          properties:
            upload_url:
              type: string
              description: Relative URL to PUT the voice audio sample to.
          required:
            - upload_url
    CreateVoiceVersionBody:
      type: object
      properties:
        configJson:
          type: string
          description: JSON config for this voice version.
  parameters: {}
paths:
  /api/v1/workspaces:
    get:
      tags:
        - Workspaces
      summary: List workspaces
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Workspace list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkspaceList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Workspaces
      summary: Create a workspace
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkspaceBody"
      responses:
        "201":
          description: Created workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}:
    get:
      tags:
        - Workspaces
      summary: Get a workspace
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      responses:
        "200":
          description: Workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags:
        - Workspaces
      summary: Update a workspace
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkspaceBody"
      responses:
        "200":
          description: Updated workspace
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/tokens:
    get:
      tags:
        - Tokens
      summary: List API tokens
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Token list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TokenList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Tokens
      summary: Create an API token
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTokenBody"
            examples:
              minimal:
                summary: Full-access key
                value:
                  name: My app
              scoped:
                summary: Read-only sessions key
                value:
                  name: Read-only sessions
                  scopes:
                    - sessions:read
              publishable:
                summary: Browser-safe embed key
                value:
                  name: My Embed Key
                  kind: publishable
                  allowedOrigins:
                    - https://example.com
      responses:
        "201":
          description: Created token — full token value only returned here
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedToken"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/tokens/{tokenId}:
    get:
      tags:
        - Tokens
      summary: Get an API token
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: tokenId
          in: path
      responses:
        "200":
          description: Token details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Token"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags:
        - Tokens
      summary: Revoke an API token
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: tokenId
          in: path
      responses:
        "200":
          description: Token revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RevokeTokenResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/devices:
    get:
      tags:
        - Devices
      summary: List trusted devices
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
      responses:
        "200":
          description: Device list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeviceList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Devices
      summary: Register a trusted device
      description: "Creates a device registry entry and mints its credential: a `dt_…`
        token scope-locked to `session:connect`, only usable to mint live
        sessions for this workspace. The token is returned once. Revoking the
        device invalidates the credential immediately."
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDeviceBody"
      responses:
        "201":
          description: Created device + credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedDevice"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/devices/{deviceId}:
    delete:
      tags:
        - Devices
      summary: Revoke a trusted device
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: deviceId
          in: path
      responses:
        "200":
          description: Device revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RevokeTokenResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/devices/{deviceId}/rotate:
    post:
      tags:
        - Devices
      summary: Rotate a device credential
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: deviceId
          in: path
      responses:
        "200":
          description: New credential
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotatedDeviceToken"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Device revoked
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/issuers:
    get:
      tags:
        - Devices
      summary: List trusted OIDC issuers
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
      responses:
        "200":
          description: Issuer list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IssuerList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Devices
      summary: Register a trusted OIDC issuer
      description: A registered issuer lets a native app present an end-user id_token
        (obtained via PKCE as a public client — no secret in the binary) as the
        POST /api/v1/sessions credential for this workspace. iss, aud, and
        signature algs are pinned here; only the JWKS is fetched at mint time.
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateIssuerBody"
      responses:
        "201":
          description: Registered issuer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrustedIssuer"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Issuer already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/issuers/{issuerId}:
    delete:
      tags:
        - Devices
      summary: Remove a trusted OIDC issuer
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: issuerId
          in: path
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/sessions:
    post:
      tags:
        - Sessions
      summary: Create a live avatar session
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSessionBody"
            examples:
              minimal:
                summary: Workspace-scoped session
                value:
                  workspace_id: 019...
              withAvatar:
                summary: Session with avatar and voice
                value:
                  workspace_id: 019...
                  avatar_ref: 019...
                  voice_ref: 019...
      responses:
        "201":
          description: Session created — connect via the returned WebSocket URL
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedSession"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Avatar or voice not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: No edge configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/sessions/{sessionId}/heartbeat:
    post:
      tags:
        - Sessions
      summary: Keep a held seat alive
      description: Refreshes the seat idle timer. Total hold time remains bounded by
        the placement hard cap and the box-enforced cap_seconds regardless of
        heartbeats.
      security:
        - bearerAuth: []
        - {}
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: sessionId
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SeatLifecycleBody"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Invalid seat token / forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/sessions/{sessionId}/release:
    post:
      tags:
        - Sessions
      summary: Release a held seat early
      description: Frees the seat immediately (promotes the queue head). Seats are
        also reclaimed automatically on disconnect and by the idle reaper — this
        just makes it instant.
      security:
        - bearerAuth: []
        - {}
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: sessionId
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SeatLifecycleBody"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                required:
                  - ok
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Invalid seat token / forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/sessions/{sessionId}:
    get:
      tags:
        - Sessions
      summary: Get a session
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: sessionId
          in: path
      responses:
        "200":
          description: Session details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Session"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/speak:
    post:
      tags:
        - Speak
      summary: Generate a lip-synced talking-head video
      description: "Synchronous, blocking call: synthesize `text` in a cloned voice
        and lip-sync it onto a face image, returning an MP4 (`video/mp4`). Media
        is supplied via OpenAI-style `content[]` (image_url +
        audio_url/input_audio) or the flat `image` + `voice_ref` fields.
        Requires the `generations:write` scope. Timing is returned in `X-*`
        response headers."
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SpeakBody"
            examples:
              contentParts:
                summary: OpenAI-style content parts (recommended)
                value:
                  workspace_id: 019...
                  text: Hi! Thanks for trying out the avatar API.
                  content:
                    - type: image_url
                      image_url:
                        url: https://example.com/face.jpg
                    - type: audio_url
                      audio_url:
                        url: https://example.com/voice.mp3
              flat:
                summary: Flat fields
                value:
                  workspace_id: 019...
                  text: The words to speak.
                  image: https://example.com/face.jpg
                  voice_ref: https://example.com/voice.mp3
      responses:
        "200":
          description: The lip-synced MP4 (binary). Timing in X-Clone-Ms / X-Gen-Ms /
            X-Total-Ms / X-Duration; X-Box-Id names the box.
          content:
            video/mp4:
              schema:
                type: string
                format: binary
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden (missing generations:write or workspace access)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: At capacity — retry after a moment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Render failed on the box
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: No box available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/prebuild:
    post:
      tags:
        - Avatars
      summary: Warm a custom avatar's box-side caches ahead of its first session
      description: "Asks every up GPU box to materialize the avatar (face/voice from
        storage) and build its idle-loop filler store in the background. Call it
        right after creating an avatar — while the user is still on your
        creation screen — so their FIRST live session starts instantly instead
        of paying an on-connect build. Returns 202 immediately; the work
        detaches box-side and defers to live viewers. Skipping it is safe: an
        unwarmed avatar still connects, it just pays the build on its first
        session. Requires the `generations:write` or `user:write` scope +
        membership of the avatar’s workspace."
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PrebuildBody"
      responses:
        "202":
          description: Warm accepted (per-box status included)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrebuildResult"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden (missing scope, non-member, workspace_mismatch, or avatar
            not in workspace)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Avatar version not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: No box accepted the warm
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrebuildResult"
        "503":
          description: No box available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/prebuild:
    post:
      tags:
        - Avatars
      summary: Warm a custom avatar's box-side caches ahead of its first session
      description: "Asks every up GPU box to materialize the avatar (face/voice from
        storage) and build its idle-loop filler store in the background. Call it
        right after creating an avatar — while the user is still on your
        creation screen — so their FIRST live session starts instantly instead
        of paying an on-connect build. Returns 202 immediately; the work
        detaches box-side and defers to live viewers. Skipping it is safe: an
        unwarmed avatar still connects, it just pays the build on its first
        session. Requires the `generations:write` or `user:write` scope +
        membership of the avatar’s workspace."
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PrebuildBody"
      responses:
        "202":
          description: Warm accepted (per-box status included)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrebuildResult"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden (missing scope, non-member, workspace_mismatch, or avatar
            not in workspace)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Avatar version not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: No box accepted the warm
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrebuildResult"
        "503":
          description: No box available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/design:
    post:
      tags:
        - Voice design
      summary: Design a voice from a text description
      description: 'Synchronous, blocking call: render a new voice described in plain
        language ("a calm warm narrator", "an excited e-sports caster") speaking
        the given text. Returns a base64 WAV. No reference audio, no
        session/seat — an authoring-time call. Requires the `generations:write`
        scope.'
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DesignVoiceBody"
            examples:
              narrator:
                summary: Calm narrator
                value:
                  description: a calm warm female narrator, gentle and clear
                  text: Hello there, this is a preview of your new voice.
              character:
                summary: Expressive character
                value:
                  description: an old wise wizard, deep gravelly voice, slow and deliberate
                  text: Ah, traveler. You have come a long way.
      responses:
        "200":
          description: Designed voice audio (base64 WAV) + timing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DesignVoiceResult"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden (missing generations:write)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: At capacity — retry after a moment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Design failed on the box
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: No box available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/avatars:
    get:
      tags:
        - Avatars
      summary: List avatars
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      responses:
        "200":
          description: Avatar list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AvatarList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Avatars
      summary: Create an avatar
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAvatarBody"
      responses:
        "201":
          description: Created avatar
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Avatar"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/avatars/{avatarId}:
    get:
      tags:
        - Avatars
      summary: Get an avatar
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      responses:
        "200":
          description: Avatar details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Avatar"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags:
        - Avatars
      summary: Rename an avatar
      description: Updates the mutable `avatars` row (currently the display name).
        Persona fields are immutable — edit them by appending a new version
        (POST .../versions).
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAvatarBody"
      responses:
        "200":
          description: Updated avatar
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Avatar"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags:
        - Avatars
      summary: Delete an avatar
      description: Soft-deletes the avatar; it stops resolving in list/get and for new
        sessions.
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      responses:
        "200":
          description: Avatar deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  id:
                    type: string
                  status:
                    type: string
                required:
                  - ok
                  - id
                  - status
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/avatars/{avatarId}/image:
    get:
      tags:
        - Avatars
      summary: Get an avatar's current face image
      description: Streams the current version's primary face image bytes (custom
        avatars have no baked poster). Resolves the current version, else the
        latest; its legacy image, else the first face row.
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      responses:
        "200":
          description: The face image (binary).
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No image / avatar not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/avatars/{avatarId}/versions:
    get:
      tags:
        - Avatars
      summary: List avatar versions
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      responses:
        "200":
          description: Avatar version list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AvatarVersionList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Avatars
      summary: Create an avatar version
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: avatarId
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAvatarVersionBody"
      responses:
        "201":
          description: Created version — PUT image bytes to upload_url
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedAvatarVersion"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Avatar not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/voices:
    get:
      tags:
        - Voices
      summary: List voices
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      responses:
        "200":
          description: Voice list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoiceList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Voices
      summary: Create a voice
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateVoiceBody"
      responses:
        "201":
          description: Created voice
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Voice"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/voices/{voiceId}:
    get:
      tags:
        - Voices
      summary: Get a voice
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: voiceId
          in: path
      responses:
        "200":
          description: Voice details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Voice"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/workspaces/{wsId}/voices/{voiceId}/versions:
    get:
      tags:
        - Voices
      summary: List voice versions
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: voiceId
          in: path
      responses:
        "200":
          description: Voice version list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoiceVersionList"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags:
        - Voices
      summary: Create a voice version
      security:
        - bearerAuth: []
      parameters:
        - schema:
            type: string
            format: uuid
          required: true
          name: wsId
          in: path
        - schema:
            type: string
            format: uuid
          required: true
          name: voiceId
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateVoiceVersionBody"
      responses:
        "201":
          description: Created version — PUT audio bytes to upload_url
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedVoiceVersion"
        "400":
          description: Bad request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Voice not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
webhooks: {}
