openapi: 3.1.0
info:
  title: bitHuman
  version: "1.0"
  contact:
    name: bitHuman Support
    url: https://www.bithuman.ai/discord
  x-logo:
    url: https://docs.bithuman.ai/bithuman-mark.png
  description: |
    Real-time, lip-synced AI avatars: audio in, talking video out — on
    the cloud, on your own hardware, or in the browser. This is the REST
    contract; the guides, the SDKs and the CLI are on
    [docs.bithuman.ai](https://docs.bithuman.ai).

    All endpoints are relative to `https://api.bithuman.ai` and
    require an `api-secret` header.
    [Get an API secret →](https://www.bithuman.ai/developer/api-keys)

    This machine-readable spec covers the developer surface. Two groups
    are documented only in the markdown guides, not here:
    [organizations](https://docs.bithuman.ai/api/organizations)
    (`/v2/organizations*`, `/v2/agents/{agent_code}/share-org`) and the
    [Realtime WebSocket relay](https://docs.bithuman.ai/api/realtime)
    (`wss://api.bithuman.ai/v1/realtime`). Purely internal / infrastructure routes —
    webhook receivers, `/func_*` generation internals, health/telemetry,
    and realtime session plumbing — are excluded entirely: they are not
    part of the public developer contract and may change without notice.

servers:
  - url: https://api.bithuman.ai
    description: Production

security:
  - apiSecret: []

paths:
  /v1/validate:
    post:
      operationId: validateApiSecret
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/validate \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # → {"valid": true}
      summary: Validate API secret
      description: |
        Verify that your API secret is valid and your account is active.

        Always returns HTTP 200. Inspect the `valid` field in the body
        to determine outcome — invalid or missing credentials yield
        `{"valid": false}` rather than a 401. The secret goes in the
        `api-secret` header; a secret sent as `Authorization: Bearer` is
        not read and also yields `{"valid": false}`.
      tags: [Authentication]
      responses:
        "200":
          description: Validation complete (check the `valid` field)
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:
                    type: boolean
                    example: true

  /v1/agent/generate:
    post:
      operationId: generateAgent
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/agent/generate \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{
                "model": "expression-2",
                "prompt": "You are a friendly fitness coach who motivates users.",
                "image": "https://example.com/face.jpg",
                "aspect_ratio": "9:16"
              }'
            # Always send model: essence-2 | expression-2 | auto (picks by subject).
            # → {"success": true, "agent_id": "A95SXN5716", "status": "processing"}
            # then poll GET /v1/agent/status/A95SXN5716
      summary: Generate a new agent
      description: |
        Create a new avatar agent from a text prompt, with optional image
        and audio assets. Poll `/v1/agent/status/{agent_id}` to check
        progress.

        **Agent creation is image-only.** Provide a portrait `image` (or let
        the prompt generate one) — bitHuman generates a **10-second
        identity video internally**, authored to loop without a seam (its
        first and last frames match). A `video` field is rejected with
        `400 VIDEO_INPUT_NOT_SUPPORTED` before anything is billed.

        **Selecting a model.** Always send `model`: `essence-2`,
        `expression-2`, `auto`, `essence-1` or `expression-1`. An omitted
        `model` creates an `expression-1` agent. A model outside your plan
        returns `403 PLAN_REQUIRED`.

        **Creation time depends on the model** (the second-generation models
        train a per-identity model, so don't apply a 5-minute client
        timeout):

        | `model` | identity input | creation time |
        |---|---|---|
        | `essence-2` | image (or generated from prompt); a 10-second identity video is generated internally | about 2–2.5 h |
        | `expression-2` | image (or generated from prompt) | about 2–2.5 h |
        | `auto` | image or prompt (classified automatically) | as the routed model (`essence-2` or `expression-2`) |
        | `essence-1` | image (or generated from prompt) | under 15 min |
        | `expression-1` (the default when `model` is omitted) | image (or generated from prompt) | about 1 min |

        Times run from this call to `status: "ready"` on
        `GET /v1/agent/status/{agent_id}`. They are a typical range, not a
        guarantee, and individual runs can take longer: size timeouts in
        hours for the second-generation models.

        **Picking the right family** ([Choosing a model](https://docs.bithuman.ai/models#choosing-a-model)):
        `essence-2` for a real person; `expression-2` for a cartoon,
        stylized, animal, creature or robot character. Send
        `model: "auto"` to apply this rule automatically — the platform
        classifies your input and records the recommended model either way.

        **Subject requirement:** every model except `expression-2` needs a
        clear, **real human face**: `essence-2`, `essence-1` and
        `expression-1` (also when `model` is omitted). The input is
        classified before anything is billed; a cartoon, stylized
        character, animal, creature or robot, or a photo with no face
        found, is rejected with `422 MODEL_SUBJECT_MISMATCH` (nothing
        charged) — use `expression-2` for those characters, or send
        `model: "auto"` to route automatically.

        Failures during generation surface as `status: "failed"` with an
        `error_message` on the status endpoint, and the creation credits
        are automatically refunded.
      tags: [Agent Generation]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string }
          description: Optional. A unique client-generated key (e.g. a UUID). Retrying with the same key returns the original response instead of starting a second billed job. Keys are retained for 24 hours.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                prompt:
                  type: string
                  description: System prompt / personality for the agent. If omitted, a random default is used.
                  example: "You are a friendly fitness coach who motivates users."
                image:
                  type: string
                  description: >-
                    URL to a face image for avatar appearance. For the best
                    likeness use a photo with one clear figure, a neutral
                    expression, facing the camera, a relaxed pose, and no
                    occlusion of the face. Every model except Expression 2
                    needs a real human face (otherwise 422
                    `MODEL_SUBJECT_MISMATCH`), and Essence 2 needs the face
                    clearly in frame (otherwise 422 `IMAGE_FACE_UNSUITABLE`),
                    before any charge.
                  example: "https://example.com/face.jpg"
                audio:
                  type: string
                  description: URL to audio for voice cloning.
                  example: "https://example.com/voice.wav"
                aspect_ratio:
                  type: string
                  enum: ["16:9", "9:16", "1:1"]
                  default: "16:9"
                  description: >-
                    Aspect ratio for the generated identity image and driver
                    video. `16:9` landscape (default), `9:16` portrait, `1:1`
                    square. Images are generated at 1080p on this ratio.
                transparency:
                  type: boolean
                  default: false
                  description: >-
                    When `true`, the identity image is generated on a solid
                    green-screen background (chroma-key friendly) so the avatar
                    can be embedded transparently — the character itself never
                    uses green. Leave `false` for a normal scene background.
                framing:
                  type: string
                  enum: [portrait, full_body]
                  default: portrait
                  description: >-
                    `portrait` (default) frames head-and-shoulders; `full_body`
                    shows the full figure including the feet — use it for
                    kiosk / standing-avatar layouts.
                agent_id:
                  type: string
                  description: Custom agent identifier. Auto-generated if omitted.
                duration:
                  type: integer
                  deprecated: true
                  description: >-
                    **Deprecated — omit it.** The internally generated
                    identity video is 10 seconds (authored to loop
                    without a seam); this parameter is ignored.
                model:
                  type: string
                  enum: [essence-2, expression-2, auto, essence-1, expression-1, essence, expression]
                  example: expression-2
                  description: >-
                    Which model to create. Always send it; if omitted, an
                    `expression-1` agent is created (250 credits). A model
                    outside your plan returns `403 PLAN_REQUIRED`. The bare
                    family names `essence` and `expression` are still accepted
                    for older integrations (they select the version-1 model);
                    new code should send a full model name.


                    **`essence-2`** is the second-generation Essence model for
                    photorealistic people — one 500-credit creation that runs
                    everywhere from on-device (CPU / Apple Silicon) up to
                    cloud GPU.
                    **`expression-2`** is the second-generation
                    expression engine — audio-driven real-time avatar video from
                    a single photo (creation trains a per-identity model —
                    about 2 hours, as does `essence-2`; see the creation-time
                    table above). **`auto`** classifies your input (the image if
                    provided, else the prompt): a photorealistic person routes to
                    `essence-2`, a cartoon / animal / creature / robot to
                    `expression-2`. Retired engine names return
                    `400 VALIDATION_ERROR`. Identity inputs and creation times
                    differ per model — see the endpoint description table and
                    [Compare models](https://docs.bithuman.ai/models).
                version:
                  type: string
                  enum: [v1, v2]
                  default: v1
                  deprecated: true
                  description: >-
                    **Older form — send a full model name in `model` instead.**
                    With a bare family name in `model` (`essence` or
                    `expression`), `v1` selects the first-generation model and
                    `v2` the second-generation one. Ignored when `model` is a
                    full model name or `auto`.
      responses:
        "200":
          description: Generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Agent generation started"
                  agent_id:
                    type: string
                    example: "A95SXN5716"
                  status:
                    type: string
                    example: "processing"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            Insufficient credits. Creation is billed **per model** —
            `expression-2` costs 2000 credits; `essence-2` costs 500;
            `auto` bills the
            routed model's rate (500 or 2000); the v1 families (`essence-1`,
            `expression-1`) cost 250. Returns error code
            `INSUFFICIENT_BALANCE`; top up at the error's `topup_url`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "400":
          description: >-
            Body failed validation — malformed JSON, an invalid / retired
            `model` value (`VALIDATION_ERROR` — the message lists the
            accepted model names), or a `video` input
            (`VIDEO_INPUT_NOT_SUPPORTED` — agent creation is image-only; the
            10-second identity video is generated internally so it loops
            without a seam; never send `video`). Rejected before dispatch; no credits
            charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                validation_error:
                  summary: Invalid model value
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "Invalid model 'my-model'; must be one of: auto, essence, essence-1, essence-2, expression, expression-1, expression-2"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
                video_input_not_supported:
                  summary: Video creation input (image-only contract)
                  value:
                    error:
                      code: "VIDEO_INPUT_NOT_SUPPORTED"
                      message: "Agent creation is image-only. Provide a portrait `image`; bitHuman generates a 10-second idle/driver video internally so it loops without a seam (first frame == last frame). Video input is not accepted for any model (essence-1, expression-1, essence-2, expression-2)."
                      httpStatus: 400
                    status: "error"
                    status_code: 400
        "403":
          description: >-
            `PLAN_REQUIRED` — the model is not in your plan (the message
            names the plan; contact sales), or a Free account, which cannot
            create agents. `AGENT_LIMIT_REACHED` — a new agent would pass your
            plan's agent limit; existing agents keep working. Both carry
            `upgrade_url`; nothing is charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                plan_required:
                  summary: A Free account creating an agent
                  value:
                    error:
                      code: "PLAN_REQUIRED"
                      message: "Creating agents starts at the Creator plan. Upgrade at https://www.bithuman.ai/pricing."
                      httpStatus: 403
                      upgrade_url: "https://www.bithuman.ai/pricing"
                    status: "error"
                    status_code: 403
                agent_limit_reached:
                  summary: The plan's agent limit
                  value:
                    error:
                      code: "AGENT_LIMIT_REACHED"
                      message: "Your Creator plan includes 7 agents and you have 7. Existing agents keep working; delete one or upgrade at https://www.bithuman.ai/pricing to create more."
                      httpStatus: 403
                      upgrade_url: "https://www.bithuman.ai/pricing"
                    status: "error"
                    status_code: 403
        "422":
          description: >-
            `MODEL_SUBJECT_MISMATCH` — a creation for a model that needs a
            real human face (`essence-2`, `essence-1`, `expression-1`, or
            `model` omitted) whose input is a cartoon, stylized character,
            animal, creature or robot, or a photo with no face found. The
            classifier runs on the image if provided, else the prompt,
            **before anything is billed** and before any agent row is
            created. Use `expression-2` for those subjects, or
            `model: "auto"` (which never rejects — it routes instead). A classifier outage never
            blocks creation (the gate fails open). `IMAGE_FACE_UNSUITABLE` — an
            Essence 2 creation whose photo has a face too small in frame, or
            several similar-sized faces; upload a closer photo of one person.
            Nothing is charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_SUBJECT_MISMATCH"
                  message: "Essence 2 needs a clear, real human face, and this image looks like a cartoon or stylized character. Use Expression 2, which animates any character: cartoons, animals, robots and stylized art. Nothing was charged."
                  httpStatus: 422
                status: "error"
                status_code: 422
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: >-
            `MODEL_NOT_YET_AVAILABLE` — the requested second-generation model
            (`essence-2`, `expression-2`, or `auto`, which
            routes only to them) can't accept creations right now. Essence 2
            and Expression 2 have been generally available since their
            July 10, 2026 launch, so this is a rare, temporary operational
            pause — returned **before any dispatch or charge**.
            The v1 families (`essence-1`, `expression-1`) always work; retry
            the v2 model shortly.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_NOT_YET_AVAILABLE"
                  message: "expression-2 isn't available for generation yet. Specify 'essence-1' or 'expression-1' to generate now."
                  httpStatus: 503
                status: "error"
                status_code: 503

  /v1/agent/status/{agent_id}:
    get:
      operationId: getAgentStatus
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/agent/status/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # poll every 5 s until data.status is "ready" (or "failed").
            # "success" is a STEP marker written mid-run — not terminal unless progress == 1.0
      summary: Get agent generation status
      description: |
        Poll this endpoint to check generation progress. Status values:
        `processing`, `generating`, `completed`, `success` (all intermediate);
        `ready` (terminal success); `failed` (terminal error).

        **Keep polling past `generating`, `completed` and `success`** — those
        are intermediate states, not terminal. `completed` can appear early
        (even around ~5% `progress`), and `success` is a STEP-level marker the
        voice/image and video steps each write (~20% and ~45% `progress`), so
        do not stop on either. Only `ready` and `failed` mean polling should
        stop — or `success` when `progress` has also reached `1.0`.

        `current_step` reports the pipeline stage: `payment` (~2%) →
        `persona` (5–15%) → `voice_image` (~20%) → `video` (~45%, the
        internally generated 10-second identity video — models that need
        one) → `lip_sync` (70–99%, the
        model-specific identity step — training/distillation for
        `expression-2` / `essence-2`, so the longest stage) → `done`
        (100%). A rare `essence-1` path reports `awaiting_face_marking`
        (~35%).

        Recommended polling interval: 5 seconds. Size your overall timeout to
        the model: minutes for the first-generation engines, **hours** for
        both second-generation ones (about 2–2.5 hours from creation to `ready`).

        **`ready` does not yet mean downloadable.** The agent can be launched
        and embedded as soon as it reports `ready`, but the downloadable
        model file is published separately, shortly afterwards. Until it is,
        `GET /v1/agent/{code}/model/download` answers
        `404 MODEL_ARTIFACT_NOT_READY` — a retryable code, not a failure. It
        can take a few minutes after `ready`; poll the download while it
        answers `404 MODEL_ARTIFACT_NOT_READY`.
      tags: [Agent Generation]
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      responses:
        "200":
          description: >-
            Agent status. `data` is the agent's full record — the same shape
            `GET /v1/agent/{code}` returns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: "#/components/schemas/Agent"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agent/{code}:
    get:
      operationId: getAgent
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/agent/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get agent details
      description: Retrieve full details for an agent by its code.
      tags: [Agent Management]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      responses:
        "200":
          description: Agent details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/EdgeBadGateway"
    post:
      operationId: updateAgent
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/agent/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"system_prompt": "You are a professional sales assistant."}'
      summary: Update an agent's prompt or providers
      description: |
        Update an existing agent's system prompt, its provider selection, or
        both. The agent must already exist (use `POST /v1/agent/generate` to
        create one). Send at least one of `system_prompt` and `providers` — a
        body with neither is rejected `400 MISSING_PARAM`.

        **An agent's `name` cannot be set here, and cannot be set at
        creation either.** Names are generated from the agent's prompt and
        portrait. A body whose only field is `name` changes nothing and
        returns `400 MISSING_PARAM`; sending `name` alongside `system_prompt`
        updates the prompt and ignores the name.
      tags: [Agent Management]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Send `system_prompt`, `providers`, or both. Any other field is ignored.
              properties:
                system_prompt:
                  type: string
                  description: New system prompt for the agent.
                  example: "You are a professional sales assistant."
                providers:
                  type: object
                  description: >-
                    Which provider each capability uses for this agent. Give a
                    capability `"default"` to use the platform's own provider,
                    or an object `{"mode": "custom", "provider_id": "<id>"}` to
                    point it at one you registered yourself. The patch is
                    merged, so a capability you omit keeps its current
                    selection. An unknown capability name, an unusable
                    selection, or a `provider_id` that is not registered on
                    your account is refused `400 VALIDATION_ERROR` at write
                    time with the reason — nothing is stored half-configured.
                  additionalProperties: false
                  properties:
                    llm:
                      $ref: "#/components/schemas/ProviderSelection"
                    stt:
                      $ref: "#/components/schemas/ProviderSelection"
                    tts:
                      $ref: "#/components/schemas/ProviderSelection"
                    realtime:
                      $ref: "#/components/schemas/ProviderSelection"
                  example:
                    llm: { mode: "custom", provider_id: "43c9a2a3-8664-4a33-a66d-77490256e7e1" }
                    tts: "default"
      responses:
        "200":
          description: Agent updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_code:
                    type: string
                    example: "A95SXN5716"
                  updated:
                    type: boolean
                    example: true
              example:
                agent_code: "A95SXN5716"
                updated: true
        "400":
          description: >-
            Nothing to update, or a `providers` patch that cannot be stored.
            `MISSING_PARAM` — the body carried neither `system_prompt` nor
            `providers` (an empty body, or one whose only field is `name`).
            `VALIDATION_ERROR` — malformed JSON, or a `providers` patch naming
            an unknown capability, an unusable selection, or a `provider_id`
            that is not registered on your account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                nothing_to_update:
                  summary: Neither field sent
                  value:
                    error:
                      code: "MISSING_PARAM"
                      message: "Nothing to update — send 'system_prompt' and/or 'providers'"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
                unregistered_provider:
                  summary: A provider id this account has not registered
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "provider_id 'does-not-exist' is not registered on this account"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      operationId: deleteAgent
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X DELETE https://api.bithuman.ai/v1/agent/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # → {"success": true, "agent_code": "A95SXN5716", "deleted": true}
      summary: Delete an agent
      description: |
        Permanently delete an agent you own. Stored assets are cleaned up
        best-effort; usage history is retained. Deleting a missing or
        non-owned agent returns 404.
      tags: [Agent Management]
      security:
        - apiSecret: []
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      responses:
        "200":
          description: Agent deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  agent_code:
                    type: string
                    example: "A95SXN5716"
                  deleted:
                    type: boolean
                    example: true
                  artifact_cleanup:
                    type: string
                    description: >-
                      How far the removal of the agent's stored assets has
                      got. `pending` means the delete has been accepted and
                      the files are still being removed in the background —
                      the agent is already gone from `GET /v1/agents` and
                      every route answers `404` for its code.
                    example: "pending"
                  billing_history:
                    type: string
                    description: >-
                      What happened to this agent's billing records. The
                      per-call rows go with the agent; the account-level
                      summaries they roll up into are retained, so your
                      invoice does not change.
                    example: "removed_summary_retained"
              example:
                success: true
                agent_code: "A95SXN5716"
                deleted: true
                artifact_cleanup: "pending"
                billing_history: "removed_summary_retained"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agent/{code}/speak:
    post:
      operationId: agentSpeak
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/agent/A95SXN5716/speak \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"message": "Hello! Welcome to our demo."}'
      summary: Make agent speak
      description: |
        Send text for the avatar to speak aloud. With `room_id` it goes to that
        one session; without it, to every deliverable session of the agent.
        The agent must be in at least one active LiveKit room.
      tags: [Agent Context]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message:
                  type: string
                  description: Text for the avatar to speak.
                  example: "Hello! Welcome to our demo."
                room_id:
                  type: string
                  description: Target a specific room (optional, defaults to all active rooms).
      responses:
        "200":
          description: Speech triggered
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_code:
                    type: string
                    example: "A95SXN5716"
                  context_type:
                    type: string
                    example: "speak"
                  delivered_to_rooms:
                    type: integer
                    example: 1
                    description: Number of active rooms that received the message.
                  rooms:
                    type: array
                    items: {type: string}
                    description: Room ids that received the message.
                    example: ["room-A95SXN5716-x1y2-z3w4"]
                  rooms_skipped:
                    type: array
                    items: {type: string}
                    description: Live rooms with no agent worker attached, so not delivered to.
                  rooms_failed:
                    type: array
                    items: {type: string}
                    description: Rooms the publish was attempted on and failed.
                  rooms_skipped_no_worker:
                    type: integer
                    description: Length of rooms_skipped.
                    example: 0
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agent/{code}/sessions:
    get:
      operationId: listAgentSessions
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/agent/A95SXN5716/sessions \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List an agent's live sessions
      description: |
        The sessions this agent has open now. Pass a returned `room_id` to
        /speak or /add-context to address ONE session instead of broadcasting.
        `deliverable` is false when the room is live but no agent worker is
        attached — /speak on such a room returns 404.
      tags: [Agent Context]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      responses:
        "200":
          description: Live sessions (possibly an empty list)
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_code:
                    type: string
                    example: "A95SXN5716"
                  sessions:
                    type: array
                    items:
                      type: object
                      properties:
                        room_id:
                          type: string
                          example: "room-A95SXN5716-x1y2-z3w4"
                        num_participants:
                          type: integer
                          example: 3
                        created_at:
                          type: integer
                          description: LiveKit room creation time, Unix seconds.
                          example: 1788480000
                        deliverable:
                          type: boolean
                          description: Whether an agent worker is attached and /speak would reach it.
                          example: true
                        matched_by:
                          type: string
                          enum: [name, ledger, "name+ledger", loose]
                          description: How the room was bound to this agent.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agent/{code}/add-context:
    post:
      operationId: addContext
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/agent/A95SXN5716/add-context \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"context": "The customer just purchased a premium plan."}'
      summary: Add context to agent
      description: |
        Inject background knowledge into the agent's context. The avatar
        won't say this aloud, but will use the information in future responses.
      tags: [Agent Context]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [context]
              properties:
                context:
                  type: string
                  description: Knowledge to inject.
                  example: "The customer just purchased a premium plan."
                type:
                  type: string
                  enum: [add_context, speak]
                  default: add_context
                  description: "`add_context` for silent knowledge (default), `speak` to trigger verbal response."
                room_id:
                  type: string
                  description: Target a specific room (optional).
      responses:
        "200":
          description: Context added
          content:
            application/json:
              schema:
                type: object
                properties:
                  agent_code:
                    type: string
                    example: "A95SXN5716"
                  context_type:
                    type: string
                    example: "add_context"
                  delivered_to_rooms:
                    type: integer
                    example: 1
                    description: Number of active rooms that received the context.
                  rooms:
                    type: array
                    items: {type: string}
                    description: Room ids that received the context.
                    example: ["room-A95SXN5716-x1y2-z3w4"]
                  rooms_skipped:
                    type: array
                    items: {type: string}
                    description: Live rooms with no agent worker attached, so not delivered to.
                  rooms_failed:
                    type: array
                    items: {type: string}
                    description: Rooms the publish was attempted on and failed.
                  rooms_skipped_no_worker:
                    type: integer
                    description: Length of `rooms_skipped`.
                    example: 0
              example:
                agent_code: "A95SXN5716"
                context_type: "add_context"
                delivered_to_rooms: 1
                rooms_skipped_no_worker: 0
                rooms: ["room-A95SXN5716-x1y2-z3w4"]
                rooms_skipped: []
                rooms_failed: []
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agent/{code}/models:
    post:
      operationId: addAgentModel
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/agent/A95SXN5716/models \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"model": "expression-2"}'
            # → {"success": true, "status": "processing", ...} — poll
            #   GET /v1/agent/status/A95SXN5716 until supported_models lists it
      summary: Add a model to an existing agent
      description: |
        Add an avatar model to an agent you already created — no need to
        re-create the agent or regenerate its persona/voice/image. The agent
        must be in the `ready` state (else `409 AGENT_NOT_READY`).

        | model | what happens | prerequisites | credits | time |
        |---|---|---|---|---|
        | `expression-1` | **immediate enablement** — the v1 engine drives the agent's existing image + voice at runtime; nothing is trained | stored image + voice + real-human subject (else `422 MODEL_SUBJECT_MISMATCH`) | **0** | immediate (this response) |
        | `expression-2` | trains the per-identity Expression 2 model from the stored image | stored image | 2000 | the same training an `expression-2` creation runs — about 2–2.5 h (see [creation times](#operation/generateAgent)) |
        | `essence-2` | trains Essence 2 from the agent's stored identity video (generated internally at creation) | stored identity video (else `422`) + photorealistic-human subject (else `422 MODEL_SUBJECT_MISMATCH`) | 500 | the same training an `essence-2` creation runs — about 2–2.5 h (see [creation times](#operation/generateAgent)) |
        | `essence-1` | builds the v1 `.imx` from the agent's stored identity video (or generates one internally from the stored image) | stored identity video or image + real-human subject (else `422 MODEL_SUBJECT_MISMATCH`) | 250 | ~10–20 min |

        Asynchronous adds return `status: "processing"` — poll
        [`GET /v1/agent/status/{agent_id}`](#tag/Agent-Generation/operation/getAgentStatus)
        until `supported_models` contains the new family. Charges are refunded
        automatically if the add fails; re-POSTing the same model never
        double-charges, and returns `status: "ready"` with `credits: 0` once
        the model already exists.
      tags: [Agent Management]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model]
              properties:
                model:
                  type: string
                  enum: [essence-1, essence-2, expression-1, expression-2]
                  description: >-
                    The model to add. Anything else returns
                    `400 VALIDATION_ERROR` listing these options.
                  example: "expression-2"
      responses:
        "200":
          description: >-
            Model added (`status: "ready"` — immediate or already available) or the
            add started (`status: "processing"` — poll supported_models).
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  agent_id:
                    type: string
                  model:
                    type: string
                  status:
                    type: string
                    enum: [ready, processing]
                  credits:
                    type: integer
                    description: Credits charged for this add (0 for immediate enablement or when already available).
                  supported_models:
                    type: array
                    items:
                      type: string
                    description: The agent's launchable model families at response time.
                  message:
                    type: string
        "400":
          description: Invalid `model` (the message lists the addable options).
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: >-
            `AGENT_NOT_READY` — the agent is still generating or failed;
            models can only be added to a ready agent.
        "422":
          description: >-
            `MODEL_PREREQUISITE_MISSING` — the agent lacks a stored asset the
            model needs (e.g. `essence-2` needs the agent's stored identity
            video — generated internally by Essence creations, never
            uploaded), or
            `MODEL_SUBJECT_MISMATCH` — `essence-2`, `essence-1` and
            `expression-1` need a real human face, and the agent's subject is
            a cartoon, stylized character, animal, creature or robot, or its
            photo has no face found (use `expression-2`). Nothing is charged.
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: >-
            `MODEL_NOT_YET_AVAILABLE` — adding a second-generation model
            (`essence-2`, `expression-2`) can't be dispatched right now.
            Both models have been generally available since the July 10, 2026
            launch, so this is a rare, temporary operational pause. Rejected
            before any charge — retry shortly. Adding the v1
            models always works.

  /v1/agent/{code}/model/download:
    get:
      operationId: downloadAgentModel
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            # 302 redirect to the artifact — -L follows it, -o names the file
            curl -L -o "A17ZTB0222.imx" \
              "https://api.bithuman.ai/v1/agent/A17ZTB0222/model/download?model=expression-2" \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # → A17ZTB0222.imx  (add ?redirect=false for the URL as JSON)
      summary: Download an agent's model artifact
      description: |
        Download the generated model file for an agent you own. The family
        defaults to the agent's own model; override with `?model=<family>`
        (canonical names, legacy names, and runtime tier slugs all fold onto
        their family). Artifact per family:

        | family | artifact |
        |---|---|
        | `essence-1` | `<code>.imx` (the portable IMX container) |
        | `essence-2` | `<CODE>.imx` — the standard Essence 2 artifact (unified IMX container; size is per identity — read `Content-Length` rather than assuming a figure; **licensed weights** — renders locally in the CLI, the Python SDK, the Android library and the Swift `Essence2` product, and the first local play checks the license with the cloud, so it needs a sign-in: https://docs.bithuman.ai/platforms/cli/voice#platform-notes). |
        | `expression-2` | `<code>.avatar` — the same container as `.imx` under a second name (a few early identities use an older format; `bithuman open` reports which) — runs locally on macOS (Apple Silicon) and Linux x86_64, or in the browser, and is also served on bitHuman's cloud |
        | `expression-1` | normally not downloadable (the shared v1 engine renders server-side from the agent's image) → `400 MODEL_NOT_DOWNLOADABLE`; an agent that went through the lip step owns a baked `<code>.imx` and is redirected to it like `essence-1` |

        The default response is a **302 redirect** to the artifact — the
        public URL for `essence-1`, a **1-hour signed URL** on a private
        bucket for the other families — so `curl -L` and browser anchors just
        work. Pass `?redirect=false` to get the URL as JSON instead.

        **Name the file yourself** (`curl -L -o "<CODE>.imx"`). The redirect
        target does not always send a `Content-Disposition` header — the
        public `essence-1` URL sends none — so a filename-from-the-header
        flag such as `curl -LOJ` silently falls back to the last path segment
        of the request URL and writes a file called `download`. With
        `?redirect=false` the JSON carries the intended name in `filename`.

        The launchability gate doubles as the download gate: a family the
        agent can't be launched as (missing from `supported_models`) returns
        `409 MODEL_NOT_GENERATED`. A family the agent supports whose artifact
        hasn't been published to the download store yet returns
        `404 MODEL_ARTIFACT_NOT_READY` — the message says when to retry;
        **poll on that code**.

        **Every grant is recorded.** A model download costs **0 credits** on
        the [pricing table](https://docs.bithuman.ai/pricing) — only
        runtime minutes bill — but the grant still writes exactly one usage row
        before the URL is issued, under the family's self-hosted pricing code,
        and hands you its id in `X-Bithuman-Meter-Id`. The rows show up in
        `GET /v1/usage` at 0 credits; they are a record of what you fetched,
        not a charge. The order matters both ways: a meter that refuses
        (`402`) or fails (`500`) returns no URL at all.

        The [bitHuman CLI](https://docs.bithuman.ai/platforms/cli/reference) wraps
        this endpoint: `bithuman pull <AGENT_CODE>` downloads the artifact,
        recognizes its model family, and prints what to do next.

        **Showcase identities need no credential.** The `essence-2` and
        `expression-2` identities listed by
        [`GET /v1/models/showcase`](#tag/Agent-Management/operation/getShowcaseManifest)
        are served to anonymous callers — same 302, same 1-hour signed URL, and
        the owner is charged **0 credits**. Every other agent still needs the
        `api-secret` header (or `Authorization: Bearer <api-secret | runtime
        token>`) and must be owned by the caller; an anonymous request for one
        answers `401 MISSING_AUTH`, the same answer an unknown code gets.
      # Showcase identities are served to anonymous callers; every other agent
      # needs the api-secret header. Both are valid, so a generated client does
      # not hard-require the header on a route that accepts a bare request.
      security:
        - apiSecret: []
        - {}
      tags: [Agent Management]
      parameters:
        - name: code
          in: path
          required: true
          schema:
            type: string
          description: >-
            Agent code. The agent must be owned by the caller (else 404) —
            except for the showcase identities in
            `GET /v1/models/showcase`, which are served with no credential.
          example: "A17ZTB0222"
        - name: model
          in: query
          required: false
          schema:
            type: string
            enum: [essence-1, essence-2, expression-2]
          description: >-
            Artifact family. Defaults to the agent's own model. The runtime
            tier slugs (`essence-2-apple`,
            `essence-2-gpu`, `essence-2-cpu`, `expression-2-gpu`; the older
            `essence-2-ane` spelling stays accepted) fold onto
            their family. `essence-2-quality` returns `400`; send
            `essence-2`. Any other unrecognized value returns the
            same `400`, listing the downloadable families.
          example: "expression-2"
        - name: member
          in: query
          required: false
          schema:
            type: string
          description: >-
            Fetch ONE on-device model member instead of the whole container.
            `essence-2` and `expression-2` publish a browser/Android bundle
            beside the container — a `web_manifest.json` and the members it
            lists (`combined_fp32.tflite`, `canon.bin`, ...) — and this serves
            a single one of them, so a browser or an Android runtime pulls only
            the files it runs. Same entitlement as the container, same 1-hour
            signed URL, and the URL honours `Range` (`curl -L -r 0-1023`
            answers `206`, so OkHttp and browsers resume). Pass
            `web_manifest.json` first to learn the member names. A member of a
            **public** agent is served to an anonymous caller, like the browser
            already serves those agents; every other visibility needs the same
            credential as the container.
          example: "web_manifest.json"
        - name: plane
          in: query
          required: false
          schema:
            type: string
            enum: [android, web]
          description: >-
            Which member set `?member=` names. `essence-2` publishes two — one
            for browsers and one for Android — and twelve names appear in both
            of them, `manifest.json` among them. A browser asking for
            `manifest.json` and a handset asking for `manifest.json` want
            different files, so for those twelve the request has to say which
            set it means. Only meaningful alongside `member=`: the container
            itself has one form, so `plane=` on its own returns
            `400 VALIDATION_ERROR`, as does any value not listed above.
            **You can usually leave it out.** A member that appears in only one
            of the two sets identifies itself — which is why the Android
            runtime's first call, `member=android_store.v1.json`, resolves with
            no `plane=` at all. When a name appears in both and the request says
            nothing, it resolves to the browser set, so every caller written
            before this parameter behaves exactly as it did.
          example: "android"
        - name: redirect
          in: query
          required: false
          schema:
            type: string
            enum: ["false", "0", "no"]
          description: >-
            Pass `false` to receive the artifact URL as JSON (below) instead
            of a 302 redirect — for UIs that want to fetch or label first.
      responses:
        "302":
          description: >-
            Redirect to the artifact — a public URL for `essence-1`, a
            1-hour signed URL for the private families. Follow it and name
            the file yourself (`curl -L -o "<CODE>.imx"`); the target does
            not always send a `Content-Disposition` header.
          headers:
            Location:
              schema:
                type: string
              description: The artifact URL.
            X-Bithuman-Meter-Id:
              schema:
                type: string
              description: >-
                The id of the single `activities` row this grant wrote. Every
                download is metered BEFORE the URL is issued — a model download
                costs **0 credits** on the published
                [pricing table](https://docs.bithuman.ai/pricing), and 0
                is a price, not a reason to skip the record. The row carries
                the family's self-hosted pricing code (e.g.
                `usage_essence_2_model_self_hosted`) and appears in
                `GET /v1/usage`; quote this id when reconciling one.
            X-Bithuman-Model:
              schema:
                type: string
              description: The canonical family actually served (`expression-2`, `essence-2`, ...).
            X-Bithuman-Model-Source:
              schema:
                type: string
              description: >-
                Why this family was chosen — `requested` when `?model=` named
                it, otherwise the agent's own default. Container arm only.
            X-Bithuman-Supported-Models:
              schema:
                type: string
              description: >-
                Comma-separated list of the other families this agent could
                have served, in the same public spellings `?model=` accepts —
                retry with one of them without a second request. Container arm
                only.
            X-Bithuman-Model-Member:
              schema:
                type: string
              description: The member name served. `?member=` arm only.
        "200":
          description: With `?redirect=false` — the artifact URL as JSON.
          headers:
            X-Bithuman-Meter-Id:
              schema:
                type: string
              description: >-
                The id of the single `activities` row this grant wrote. Every
                download is metered BEFORE the URL is issued — a model download
                costs **0 credits** on the published
                [pricing table](https://docs.bithuman.ai/pricing), and 0
                is a price, not a reason to skip the record. The row carries
                the family's self-hosted pricing code (e.g.
                `usage_essence_2_model_self_hosted`) and appears in
                `GET /v1/usage`; quote this id when reconciling one.
            X-Bithuman-Model:
              schema:
                type: string
              description: The canonical family actually served (`expression-2`, `essence-2`, ...).
            X-Bithuman-Model-Source:
              schema:
                type: string
              description: >-
                Why this family was chosen — `requested` when `?model=` named
                it, otherwise the agent's own default. Container arm only.
            X-Bithuman-Supported-Models:
              schema:
                type: string
              description: >-
                Comma-separated list of the other families this agent could
                have served, in the same public spellings `?model=` accepts —
                retry with one of them without a second request. Container arm
                only.
            X-Bithuman-Model-Member:
              schema:
                type: string
              description: The member name served. `?member=` arm only.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      code:
                        type: string
                        example: "A17ZTB0222"
                      model:
                        type: string
                        description: The resolved canonical family.
                        example: "expression-2"
                      filename:
                        type: string
                        example: "A17ZTB0222.imx"
                      url:
                        type: string
                        description: The artifact URL (signed, 1-hour TTL, for the private families).
                      expires_in:
                        type: [integer, "null"]
                        description: Signed-URL TTL in seconds (3600); `null` for the public essence-1 URL.
                        example: 3600
        "400":
          description: >-
            `VALIDATION_ERROR` — unknown `model` value (the message lists the
            downloadable families), or `MODEL_NOT_DOWNLOADABLE` — the family
            has no per-identity artifact to download (`expression-1` renders
            server-side from the agent's image).
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            `INSUFFICIENT_BALANCE` — the download could not be metered against
            your account. Every grant writes its usage row BEFORE the URL is
            issued, so a refused meter returns **no URL at all** rather than an
            artifact nobody can account for. A download itself costs 0 credits;
            this is what a suspended or depleted account sees.
        "404":
          description: >-
            `NOT_FOUND` — agent unknown or not owned by this account; or
            `MODEL_ARTIFACT_NOT_READY` — the family is supported but its
            artifact hasn't been published to the download store yet. The
            message says when to retry; poll on this code.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_ARTIFACT_NOT_READY"
                  message: "agent A17ZTB0222's expression-2 artifact isn't available for download yet: the Mac-runnable build is paced, typically within the hour"
                  httpStatus: 404
                status: "error"
                status_code: 404
        "409":
          description: >-
            `MODEL_NOT_GENERATED` — the requested family isn't in the agent's
            `supported_models` (same gate as embed/session launch), e.g.
            `agent <code>'s expression-2 model hasn't been generated yet`
            — create with, or add, that model to generate it.
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: >-
            `INTERNAL_ERROR` — the artifact could not be resolved, or the usage
            row could not be written. In both cases **no URL is issued**: a
            grant we cannot record is not handed out. Safe to retry.
        "503":
          description: >-
            `SERVICE_UNAVAILABLE` — the download service briefly lost its
            connection to storage and could not recover within the request.
            **No URL is issued and nothing is metered.** Retry after the
            `Retry-After` delay; the same request then succeeds.
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retrying (currently 2).

  /v1/files/upload:
    post:
      operationId: uploadFile
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/files/upload \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"file_url": "https://example.com/avatar.jpg", "file_type": "image"}'
      summary: Upload a file
      description: |
        Upload images, videos, audio, or documents — by public URL or as
        base64. Files are organized by type, and the type is read from the
        file's own bytes: `file_type` and the filename extension are both
        cross-checked against them, never trusted over them. The response
        returns a public URL for the stored file.
      tags: [Files]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  title: Upload by URL
                  required: [file_url]
                  properties:
                    file_url:
                      type: string
                      description: Public URL to download the file from.
                      example: "https://example.com/avatar.jpg"
                    file_type:
                      type: string
                      enum: [auto, image, video, audio, document, pdf]
                      default: auto
                      description: >-
                        Optional cross-check, not an override — the stored
                        category always comes from the file's own bytes. Give a
                        value and the bytes must agree with it, or the upload is
                        refused with `415`. `document` and `pdf` are the same
                        category. Any other value is refused with `400
                        VALIDATION_ERROR`; omit it, or send `auto`, to let the
                        bytes decide. The filename here comes from the URL's
                        path, and its extension must be one of the supported
                        ones.
                - type: object
                  title: Upload by base64
                  required: [file_data, file_name]
                  properties:
                    file_data:
                      type: string
                      description: Base64-encoded file data.
                    file_name:
                      type: string
                      description: >-
                        Original filename. The extension is required and must
                        be one of the supported ones (see `415`); a name
                        without one is refused.
                      example: "avatar.jpg"
                    file_type:
                      type: string
                      enum: [auto, image, video, audio, document, pdf]
                      default: auto
                      description: >-
                        Optional cross-check, not an override — the stored
                        category always comes from the file's own bytes. Give a
                        value and the bytes must agree with it, or the upload is
                        refused with `415`. `document` and `pdf` are the same
                        category. Any other value is refused with `400
                        VALIDATION_ERROR`; omit it, or send `auto`, to let the
                        bytes decide.
      responses:
        "200":
          description: File uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: "File uploaded successfully"
                  data:
                    type: object
                    properties:
                      file_url:
                        type: string
                        description: Public CDN URL of the uploaded file.
                      original_source:
                        type: string
                        description: Original URL or filename.
                      file_type:
                        type: string
                        description: Detected file type category.
                      file_size:
                        type: integer
                        description: File size in bytes.
                      mime_type:
                        type: string
                        description: MIME content type.
                      asset_category:
                        type: string
                        description: Storage category (images, videos, audio, docs).
                      uploaded_at:
                        type: string
                        format: date-time
        "400":
          description: |
            One of:

            - `DOWNLOAD_FAILED` — `file_url` could not be downloaded:
              unreachable, not publicly readable, or not an ordinary public
              `http(s)` address.
            - `VALIDATION_ERROR` — `file_data` was not valid base64, both
              `file_url` and `file_data` were sent, or `file_type` was a value
              this API does not define. The accepted values are `auto`,
              `image`, `video`, `audio`, `document` and `pdf`; anything else is
              refused rather than quietly treated as `auto`, so a typo cannot
              silently change where your file lands.
            - `MISSING_PARAM` — neither `file_url` nor `file_data` was sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "VALIDATION_ERROR"
                  message: "Invalid file_type 'picture'; must be one of: auto, image, video, audio, document, pdf"
                  httpStatus: 400
                status: "error"
                status_code: 400
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          description: |
            `FILE_TOO_LARGE` — the file is over the limit for its category:
            **10 MB** images, **100 MB** video, **25 MB** audio and documents.
            The category is read from the file's own bytes, so the limit that
            applies is the one for what the file actually is.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "FILE_TOO_LARGE"
                  message: "file is 31 MB; the limit for image is 10 MB"
                  httpStatus: 413
                status: "error"
                status_code: 413
        "415":
          description: |
            `UNSUPPORTED_TYPE` — this API will not store the file. Four things
            raise it:

            - **The bytes are not a supported type.** Supported: JPEG, PNG,
              GIF, WebP, BMP · MP4, MOV, WebM, AVI, WMV, FLV, Matroska ·
              MP3, WAV, FLAC, AAC, OGG, M4A · PDF, Word, Excel, PowerPoint,
              RTF and plain text.
            - **The bytes contradict `file_type`.** Sending
              `file_type: "image"` with a text file is refused here rather than
              stored under the wrong category.
            - **The filename extension is not one of the supported ones.** For
              a base64 upload that is `file_name`; for a URL upload it is the
              last path segment of `file_url`. A name with no extension, or
              one like `.exe` or `.sh`, is refused.
            - **The file would run in a browser.** SVG, HTML and any text
              carrying a script are refused outright: an uploaded file is
              served from a public address, and a file that executes there
              would run as if it came from us.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "UNSUPPORTED_TYPE"
                  message: "file_type was declared 'image' but the uploaded bytes are text/plain"
                  httpStatus: 415
                status: "error"
                status_code: 415
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          description: >-
            `INTERNAL_ERROR` — the file was accepted but could not be stored.
            Nothing is kept and nothing is charged; retrying is safe.

  /v1/me:
    get:
      operationId: getMe
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/me \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get current account
      description: >-
        The canonical account preflight — returns your identity, plan, and live
        credit balance in one call. Use it to show "who am I / how many credits"
        and to gate features before a billable call.
      tags: [Account]
      responses:
        "200":
          description: Account retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      user_id:
                        type: string
                      email:
                        type: string
                      plan:
                        type: string
                        example: "pro"
                      plan_code:
                        type: string
                        example: "membership_pro"
                      credit_balance:
                        type: integer
                        example: 1240
                        description: Total spendable credits = plan + topup + reward.
                      plan_credits_remaining:
                        type: integer
                        example: 1000
                      topup_credits_remaining:
                        type: integer
                        example: 240
                      account_status:
                        type: string
                        example: "active"
                      upgrade_url:
                        type: string
                      pricing_url:
                        type: string
                  status:
                    type: string
                    example: "success"
                  status_code:
                    type: integer
                    example: 200
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimitedByAddress"

  /v1/models/showcase:
    get:
      operationId: getShowcaseManifest
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            # No credential, no account. Every entry is downloadable by anyone.
            curl -s https://api.bithuman.ai/v1/models/showcase | jq '.models[] | {slug, model, size}'
        - lang: shell
          label: bitHuman CLI
          source: |
            bithuman list                              # this catalogue, with what is cached
            bithuman pull bolt                         # download one ahead of time
            bithuman render bolt hello.wav -o out.mp4  # or let render download it
      summary: List the freely downloadable showcase models
      description: |
        The catalogue of **Essence 2** and **Expression 2** showcase identities
        whose model weights anyone may download — no api-secret, no account.

        Each entry's `url` is
        [`GET /v1/agent/{code}/model/download`](#tag/Agent-Management/operation/downloadAgentModel),
        which answers a **302** to a 1-hour signed URL for anonymous callers
        too. The grant is still recorded (one usage row) and costs the agent's
        owner **0 credits** — an anonymous download is free or refused, never
        billed to the owner.

        The list is generated from the download route's own admission
        predicate, so a code listed here is a code the route serves: an
        `essence-2` or `expression-2` identity that is ready, listed in the
        agent gallery, owns its own artifact, and whose owner has authorised
        anonymous distribution of the weights. Agents that are merely public in
        the gallery are **not** here — being viewable is a different permission
        from the weights being distributable.

        The shape is the bitHuman CLI's manifest v2; `bithuman list` and
        `bithuman pull` read this catalogue.

        **Performance.** The reply carries no frame rates.
        `note_performance` maps each family in the catalogue to the
        [performance page](https://docs.bithuman.ai/performance), where the frame rates are
        measured and published for every device.

        `sha256` is deliberately empty: no content digest is recorded for these
        objects yet, and the CLI skips verification on an empty value rather
        than failing. Verify `size` and the file itself (`bithuman open`)
        instead.

        Throttled per IP; the reply is cached for 60 seconds.
      tags: [Agent Management]
      security: []
      responses:
        "200":
          description: The showcase catalogue.
          content:
            application/json:
              schema:
                type: object
                properties:
                  version:
                    type: integer
                    example: 2
                  updated:
                    type: string
                    example: "2026-09-10"
                  source:
                    type: string
                  selection_criteria:
                    type: string
                  note_performance:
                    type: object
                    additionalProperties:
                      type: string
                  models:
                    type: array
                    items:
                      type: object
                      properties:
                        slug:
                          type: string
                          example: "bolt"
                        name:
                          type: string
                          example: "Bolt"
                        description:
                          type: string
                        model:
                          type: string
                          enum: [essence-2, expression-2]
                        agent_code:
                          type: string
                          example: "X03BOLT"
                        category:
                          type: string
                        is_featured:
                          type: boolean
                        url:
                          type: string
                          example: "https://api.bithuman.ai/v1/agent/X03BOLT/model/download?model=expression-2"
                        sha256:
                          type: string
                          description: Empty — no digest is published for these objects yet.
                        size:
                          type: integer
                          example: 198106157
                        votes:
                          type: integer
                        image_url:
                          type: string
                          description: >-
                            The identity's public poster (its thumbnail, else its
                            image) — the picture a gallery card shows. Empty when
                            the identity has neither.
                        gender:
                          type: string
                          description: >-
                            The identity's stored gender (e.g. `feminine`,
                            `masculine`), for matching a voice. Empty when unset.
        "429":
          $ref: "#/components/responses/RateLimitedByAddress"

  /v1/pricing:
    get:
      operationId: getPricing
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/pricing \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get credit pricing
      description: >-
        Machine-readable credit pricing for billable operations, so you can
        estimate cost before a call. Authoritative charges are always enforced
        server-side at request time. For forward per-minute realtime estimates
        based on your current balance, see `GET /v2/credit-summaries`
        (`minutes_estimate`).
      tags: [Account]
      responses:
        "200":
          description: Pricing retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  data:
                    type: object
                    properties:
                      unit:
                        type: string
                        example: "credits"
                      agent_generation:
                        type: object
                        description: >-
                          Agent creation is priced **per model** — read
                          `by_model` (there is no flat rate). `expression-2`
                          costs 2000 credits; the Essence 2 family costs 500
                          (`essence-2` is the combined Essence 2 creation —
                          one 500-credit charge covers both models); the v1
                          families cost 250. `auto` charges the routed
                          model's rate (500 for `essence-2`, 2000 for
                          `expression-2`). Post-generation adds via
                          `POST /v1/agent/{code}/models` charge the same
                          per-model rates (adding `expression-1` is free).
                        properties:
                          unit:
                            type: string
                            example: "credits"
                          by_model:
                            type: object
                            additionalProperties:
                              type: integer
                            description: >-
                              One-time creation cost per canonical `model`
                              value accepted by `POST /v1/agent/generate`.
                              `essence-2-quality` returns a `400`.
                            example:
                              essence-1: 250
                              expression-1: 250
                              expression-2: 2000
                              essence-2: 500
                              auto: 2000
                          note:
                            type: string
                      talking_video:
                        type: object
                        properties:
                          unit:
                            type: string
                            example: "credits_per_minute"
                          billing:
                            type: string
                            example: "ceil(minutes) * rate, minimum 1 minute"
                          rates:
                            type: object
                            additionalProperties:
                              type: integer
                            example:
                              essence-1: 2
                              expression-1: 4
                              expression-2: 4
                              essence-2: 4
                      dynamics_generation:
                        type: object
                        properties:
                          flat:
                            type: integer
                            example: 250
                          note:
                            type: string
                      notes:
                        type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v2/credit-summaries:
    get:
      operationId: getCreditSummaries
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v2/credit-summaries \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Check credit balance
      description: |
        Returns the current credit balance, plan details, and estimated minutes
        remaining for each usage type. Always scoped to the **authenticated
        account** (the owner of the `api-secret`) — there is no way to look up
        another user.
      tags: [Billing]
      security:
        - apiSecret: []
      parameters:
      responses:
        "200":
          description: Credit balance retrieved
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      user_id:
                        type: string
                        description: The authenticated account's UUID.
                      balance:
                        type: number
                        format: float
                        description: Sum of plan + topup + reward credits, returned as a float (e.g. 1240.0).
                        example: 1240.0
                      plan_credits:
                        type: number
                        format: float
                      topup_credits:
                        type: number
                        format: float
                      minutes_estimate:
                        type: object
                        description: >-
                          Advisory forward runway: balance divided by each
                          serving mode's credits/min rate. One key per model and
                          hosting mode (essence_2_cloud, essence_2_self_hosted,
                          expression_2_cloud, expression_2_self_hosted,
                          essence_1_cloud, essence_1_self_hosted,
                          expression_1_cloud, expression_1_self_hosted), plus
                          voice_chat and camera_chat for managed agents. The
                          unversioned essence_* / expression_* keys are retained
                          aliases for the FIRST-generation models - they are NOT
                          the Essence 2 rate.
                        additionalProperties:
                          type: integer
                      is_enterprise:
                        type: boolean
                      isEnterprisePlanUser:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/embed-tokens/request:
    post:
      operationId: requestEmbedToken
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/embed-tokens/request \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"agent_id": "A23WJF0199", "fingerprint": "<device-fingerprint>"}'
            # → data.token is a 1-hour JWT for the embed widget
      summary: Generate embed token
      description: Generate a JWT token for embedding an avatar on a website. Use this from your backend — never expose your API secret in frontend code.
      tags: [Embedding]
      security:
        - apiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - agent_id
                - fingerprint
              properties:
                agent_id:
                  type: string
                  description: Agent code (e.g. A23WJF0199)
                  example: "A23WJF0199"
                fingerprint:
                  type: string
                  description: Stable per-device hex string used for session tracking and per-visitor rate limiting. Required.
                  example: "<device-fingerprint>"
                model:
                  type: string
                  description: >-
                    Optional. Request a specific avatar model for the embed
                    session — a model name (`essence-1`, `expression-1`,
                    `essence-2`, `expression-2`) or a
                    force-tier slug that pins one serving tier
                    (`expression-2-gpu/-cpu/-apple`, `essence-2-gpu/-apple/-cpu`; the older
                    `-ane` spelling of the Apple tier stays accepted). This is the **early
                    artifact gate**: if the requested model folds onto the
                    `expression-2` / `essence-2` families and the
                    agent's trained per-identity model hasn't been generated
                    yet, the request is rejected `409 MODEL_NOT_GENERATED`
                    instead of failing later at session dispatch. Unknown
                    values return `400 VALIDATION_ERROR` listing the accepted
                    names. Omitted → the agent's own default model.
                  example: "expression-2"
      responses:
        "200":
          description: Token generated. The returned `token` is a JWT (HS256-signed) with a 1-hour TTL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: success
                  status_code:
                    type: integer
                    example: 200
                  data:
                    type: object
                    properties:
                      token:
                        type: string
                        description: 1-hour JWT to pass as the `data-token` attribute on the embed widget script tag.
                      sid:
                        type: string
                        description: Session identifier for tracking this embed instance.
                      model:
                        type: string
                        description: The model baked into the token claim (present when `model` was requested).
                      supported_models:
                        type: array
                        items: { type: string }
                        description: >-
                          Canonical model families this agent can be launched
                          as right now (e.g. a ready expression-2 agent lists
                          `expression-2`). Tier slugs inherit their family
                          and are not listed.
                        example: ["essence-2", "expression-2"]
        "400":
          description: >-
            `MISSING_PARAM` — the body left out `agent_id` or `fingerprint`;
            both are required and neither has a default.
            `VALIDATION_ERROR` — the `model` field names something that is not
            an accepted model; the message lists the names that are.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missing_fingerprint:
                  summary: No fingerprint
                  value:
                    error:
                      code: "MISSING_PARAM"
                      message: "Missing fingerprint parameter"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
                missing_agent_id:
                  summary: No agent_id
                  value:
                    error:
                      code: "MISSING_PARAM"
                      message: "Missing agent_id parameter"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
                unknown_model:
                  summary: A model name that does not exist
                  value:
                    error:
                      code: "VALIDATION_ERROR"
                      message: "Invalid model 'essence-3'; must be one of: essence-1, expression-1, essence-2, expression-2 (legacy names and tier slugs are still accepted)"
                      httpStatus: 400
                    status: "error"
                    status_code: 400
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: >-
            `NOT_FOUND` — no agent matches `agent_id` under this API secret.
            A **deleted** agent answers this too, so a delete takes the agent
            out of the embed path immediately.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "NOT_FOUND"
                  message: "Agent not found for code: A95SXN5716"
                  httpStatus: 404
                status: "error"
                status_code: 404
        "409":
          description: >-
            `MODEL_NOT_GENERATED` — the requested `model` folds to a known
            family the agent can't be launched as right now (it is missing
            from the agent's `supported_models`). For the trained families
            (`expression-2` / `essence-2`) the message is `agent
            <code>'s <model> model hasn't been generated yet`. Add the
            model (`POST /v1/agent/{code}/models`) or create the agent with
            it, then retry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_NOT_GENERATED"
                  message: "agent A56ZFX6217's expression-2 model hasn't been generated yet"
                  httpStatus: 409
                status: "error"
                status_code: 409
        "429":
          $ref: "#/components/responses/RateLimitedByAddress"

  /v1/runtime-tokens/mint:
    post:
      operationId: mintScopedRuntimeToken
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/runtime-tokens/mint \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"agent_code": "A80HVD8577"}'
            # → {"scoped_token": "...", "user_id": "...", "agent_code": "A80HVD8577", "expires_at": 1757280000}
      summary: Mint a scoped runtime token for one agent
      description: |
        Exchange your API secret, **server-side**, for a 1-hour token bound to a
        single `agent_code` — so a browser, a phone app or any other client you
        do not fully control never holds your master secret.

        This is the credential the on-device paths accept. It works as
        `Authorization: Bearer <scoped_token>` on
        [the model download route](#operation/downloadAgentModel), including
        the `?member=` fetch the browser and Android runtimes use.

        The scope is real: presenting the token for a **different** agent code
        answers `404`, exactly like a code you do not own. It expires in one
        hour and is not refreshable — mint another.

        **LiveKit plugin.** With `"scope": "livekit-cloud"` this mints the token
        to pass to `livekit-plugins-bithuman` as `api_secret` instead of your API
        secret. The plugin writes that value into LiveKit room attributes, where
        every participant can read it. This token can only start this agent's
        cloud avatar session. It is also bound to `room_name` and `livekit_url`
        when you send them, and it bills your account. For another agent, room
        or LiveKit server it answers `403`. It cannot download the model or call
        any other endpoint. See [LiveKit integration](/platforms/livekit).

        Call this from your server. Never ship the api-secret this endpoint
        consumes to a client.
      tags: [Authentication]
      security:
        - apiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_code]
              properties:
                agent_code:
                  type: string
                  example: "A80HVD8577"
                  description: The one agent this token may act for.
                scope:
                  type: string
                  enum: [runtime-heartbeat, livekit-cloud]
                  default: runtime-heartbeat
                  description: >-
                    `livekit-cloud` mints the launch-only token for the LiveKit
                    plugin's cloud mode. Omit it for the runtime token described
                    above.
                room_name:
                  type: string
                  description: >-
                    `livekit-cloud` only. Binds the token to this LiveKit room.
                livekit_url:
                  type: string
                  example: "wss://your-project.livekit.cloud"
                  description: >-
                    `livekit-cloud` only. Binds the token to this LiveKit
                    server. It must be the URL the plugin connects to.
      responses:
        "200":
          description: A scoped token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  scoped_token: { type: string }
                  user_id: { type: string }
                  agent_code: { type: string, example: "A80HVD8577" }
                  expires_at:
                    type: integer
                    description: Unix seconds; one hour from minting.
        "400":
          description: "`agent_code` is required, or `scope` is not one of the two values."
        "401":
          description: Missing or invalid api-secret.

  /v1/runtime-tokens/request:
    post:
      operationId: requestRuntimeToken
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/runtime-tokens/request \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"agent_code": "A80HVD8577"}'
            # → data.token is a short-lived runtime JWT the SDK uses to run a session
        - lang: shell
          label: cURL (start a cloud avatar)
          source: |
            curl -X POST https://api.bithuman.ai/v1/runtime-tokens/request \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"mode": "gpu", "model": "essence-2", "agent_id": "<your agent code>",
                   "livekit_url": "wss://your-project.livekit.cloud",
                   "livekit_token": "<avatar join token>", "room_name": "your-room"}'
            # → {"avatar_session_started": true, "model": "essence-2", ...}
      summary: Request a runtime token, or start a cloud avatar
      description: >-
        Exchange your API secret for a short-lived runtime token that authorizes one
        avatar session. The SDKs call this for you; call it directly when building a
        custom runtime. Free to call; the session it authorizes is billed per
        `GET /v1/pricing`.


        **With `mode`, the same endpoint starts a cloud avatar** in your LiveKit
        room instead: send `mode`, `agent_id` (or `image` for an Expression 1
        photo), `livekit_url`, `livekit_token` and `room_name`. The avatar joins
        as `bithuman-avatar-agent` and lip-syncs the audio you stream to it. The
        token, audio and latency rules are on
        [Cloud avatar without the plugin](https://docs.bithuman.ai/platforms/livekit/cloud-avatar).


        **`fingerprint` must be at least 32 characters** when you send it.
        A shorter one is rejected `400 VALIDATION_ERROR` — a common surprise
        when a custom runtime passes a short device id. It is optional: leave
        it out rather than send a short one.
      tags: [Authentication]
      security:
        - apiSecret: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              oneOf:
                - title: Runtime token
                  type: object
                  properties:
                    agent_code:
                      type: string
                      description: Agent code the session runs (attribution + transcript).
                      example: "A80HVD8577"
                    billing_type:
                      type: string
                      description: Selects the model and pricing schedule for the session.
                      example: "agent-voice-chat"
                    room_name:
                      type: string
                      description: LiveKit room name to associate the session with its transcript.
                    user_id:
                      type: string
                      description: Optional. Attribute billing to a specific end-user (owner override).
                    fingerprint:
                      type: string
                      minLength: 32
                      description: >-
                        Optional runtime-instance fingerprint (analytics only).
                        When present it must be **at least 32 characters** —
                        a shorter value is rejected `400 VALIDATION_ERROR`
                        ("String should have at least 32 characters"), so omit the
                        field rather than send a short device id.
                      example: "<device-fingerprint>"
                - title: Start a cloud avatar
                  type: object
                  required: [mode]
                  properties:
                    mode:
                      type: string
                      description: >-
                        `gpu`. Sending `mode` is what makes this call start an
                        avatar. The older value `cpu` also works and requires
                        `agent_id`.
                      example: "gpu"
                    model:
                      type: string
                      description: >-
                        The model to render: essence-1, essence-2, expression-1
                        or expression-2. Recommended. Left out, the server picks
                        from `mode` and the agent's models; the response's `model`
                        says which one launched.
                      example: "essence-2"
                    agent_id:
                      type: string
                      description: The agent code. Required unless `image` is sent.
                    image:
                      type: string
                      description: >-
                        Expression 1 only, with no `agent_id`: a portrait as a public
                        http(s) URL (fetched with a plain GET, must answer 200 without
                        authentication), raw base64, or a `data:image/...;base64,` URI.
                        JPEG or PNG, one face, head and shoulders.
                    livekit_url:
                      type: string
                      description: Your LiveKit server URL.
                      example: "wss://your-project.livekit.cloud"
                    livekit_token:
                      type: string
                      description: >-
                        The avatar's LiveKit join token: identity
                        `bithuman-avatar-agent`, kind `agent`, attribute
                        `lk.publish_on_behalf` set to your audio sender's identity.
                    room_name:
                      type: string
                      description: The LiveKit room the avatar joins.
                      example: "your-room"
                    version:
                      type: string
                      description: "`v1` or `v2`, with a bare family name only (`essence` + `v2` = essence-2)."
      responses:
        "200":
          description: >-
            A runtime token (`data.token` is a short-lived JWT for one session), or,
            when the body has `mode`, the cloud avatar is starting: a flat object
            with `avatar_session_started: true` and `model`, the model the session
            launched as, plus echoes of `mode`, `agent_id` and `image` and your
            `user_id`. Until 2026-10-12, a Free account's runtime token and cloud
            avatar start also carry `plan_notice` and the `X-Bithuman-Plan-Notice`
            header; show it as sent. From 2026-10-12, a Free account's cloud avatar
            start is refused with `403 PLAN_REQUIRED` before anything is launched or
            billed.
          headers:
            X-Bithuman-Plan-Notice:
              schema:
                type: string
              description: >-
                The same text as `plan_notice`: "Free-plan API and SDK access
                ends on 2026-10-12. Upgrade at https://www.bithuman.ai/pricing
                to keep it.", or, for a Free account keeping access on top-up
                credits bought before 2026-09-27, the wording that says so.
          content:
            application/json:
              schema:
                oneOf:
                  - title: Runtime token
                    type: object
                    properties:
                      data:
                        type: object
                        properties:
                          token: { type: string }
                          transaction_id: { type: string }
                      status: { type: string, example: success }
                      status_code: { type: integer, example: 200 }
                  - title: Cloud avatar started
                    type: object
                    properties:
                      avatar_session_started: { type: boolean, example: true }
                      model: { type: string, example: "essence-2" }
                      mode: { type: string, example: "gpu" }
                      agent_id: { type: string, nullable: true }
                      image: { type: string, nullable: true }
                      user_id: { type: string }
                      plan_notice: { type: string, description: "Free accounts, until 2026-10-12." }
                      session_id: { type: string, example: "cs_4f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e", description: "End or read the session by this id: /v1/runtime-sessions/{session_id}." }
              examples:
                runtimeToken:
                  summary: Runtime token
                  value:
                    data:
                      transaction_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                      token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                    status: success
                    status_code: 200
                cloudAvatar:
                  summary: Cloud avatar started
                  value:
                    avatar_session_started: true
                    model: "essence-2"
                    mode: "gpu"
                    agent_id: "<your agent code>"
                    image: null
                    user_id: "<your user id>"
        "400":
          description: >-
            `VALIDATION_ERROR` — a field failed validation. The most common
            cause is a `fingerprint` shorter than 32 characters; the message
            names the field and the rule. For a cloud avatar, also: `image`
            without `agent_id` on a model other than Expression 1, or an
            Essence 1 session without `livekit_url`, `livekit_token` and
            `room_name`; nothing is launched or billed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "VALIDATION_ERROR"
                  message: "Validation failed: fingerprint: String should have at least 32 characters"
                  httpStatus: 400
                status: "error"
                status_code: 400
        "402":
          description: >-
            `INSUFFICIENT_BALANCE` — not enough credits to start. Top up at the
            error's `topup_url`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "INSUFFICIENT_BALANCE"
                  message: "Insufficient balance to complete this action. Please add funds to continue."
                  httpStatus: 402
                  topup_url: "https://www.bithuman.ai/billing#credits"
                status: "error"
                status_code: 402
        "403":
          description: >-
            `PLAN_REQUIRED` — from 2026-10-12, a Free account's API secret, or
            (cloud avatar) a model not in your plan. `CONCURRENCY_LIMIT_REACHED`
            — a new cloud session would pass your plan's concurrent sessions;
            live sessions are never cut off. Both carry `upgrade_url`.
            `RUNTIME_SUSPENDED` — the key was revoked, or runtime access is
            suspended (no link; a plan change does not clear it).
            `ACCOUNT_SUSPENDED` — the balance is too far below zero.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                plan_required:
                  summary: A Free account from 2026-10-12
                  value:
                    error:
                      code: "PLAN_REQUIRED"
                      message: "API and SDK access starts at the Creator plan. Upgrade at https://www.bithuman.ai/pricing to keep using your API secret."
                      httpStatus: 403
                      upgrade_url: "https://www.bithuman.ai/pricing"
                    status: "error"
                    status_code: 403
                concurrency_limit_reached:
                  summary: The plan's concurrent cloud sessions
                  value:
                    error:
                      code: "CONCURRENCY_LIMIT_REACHED"
                      message: "Your Creator plan allows 3 cloud avatar sessions at once, and 3 are running (3/3). End a session, or upgrade your plan at https://www.bithuman.ai/pricing."
                      details:
                        tier: "Creator"
                        cap: 3
                        active: 3
                        upgrade_url: "https://www.bithuman.ai/pricing"
                      upgrade_url: "https://www.bithuman.ai/pricing"
                      httpStatus: 403
                    status: "error"
                    status_code: 403
        "404":
          description: "Cloud avatar: no agent with this `agent_id` that you can start."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: >-
            Cloud avatar: `VALIDATION_ERROR`, the agent cannot be served as the
            requested model, or its own model is not ready yet. Nothing is
            launched or billed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: >-
            Cloud avatar: no capacity right now, or a temporary failure. Retry
            after the `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v1/dynamics/generate:
    post:
      operationId: generateDynamics
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/dynamics/generate \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"agent_id": "A95SXN5716"}'
            # → {"success": true, "status": "processing"} — poll GET /v1/dynamics/A95SXN5716
      summary: Generate gesture animations
      description: |
        Create gesture animations (wave, nod, smile, laugh, etc.) for an agent.
        These can be triggered during live sessions via keyword mapping.
      tags: [Dynamics]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string }
          description: Optional. A unique client-generated key (e.g. a UUID). Retrying with the same key returns the original response instead of starting a second billed job. Keys are retained for 24 hours.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_id]
              properties:
                agent_id:
                  type: string
                  example: "A95SXN5716"
                image_url:
                  type: string
                  description: Source image URL. Optional — defaults to the agent's primary image if omitted.
                duration:
                  type: integer
                  default: 5
                  description: Gesture duration in seconds.
                model:
                  type: string
                  enum: [seedance, auto]
                  default: seedance
                  description: >-
                    Video motion model. `seedance` (Seedance 1.5 pro) is the only
                    dynamics model; `auto` resolves to it. Any legacy value (e.g.
                    the retired `kling`) is coerced to `seedance`.
      responses:
        "200":
          description: Dynamics generation started
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: "Dynamics generation started"
                  agent_id:
                    type: string
                  status:
                    type: string
                    example: "processing"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Insufficient credits. Dynamics generation costs 250 credits — INSUFFICIENT_BALANCE.
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/dynamics/{agent_id}:
    get:
      operationId: getDynamics
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/dynamics/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get available gestures
      description: List all available gesture animations for an agent.
      tags: [Dynamics]
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
          example: "A95SXN5716"
      responses:
        "200":
          description: Available gestures
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      agent_id:
                        type: string
                      url:
                        type: string
                        description: Model path or null.
                      status:
                        type: string
                        enum: [ready, generating, processing, failed]
                      gestures:
                        type: object
                        description: Map of gesture name to video URL.
                        additionalProperties:
                          type: string
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    put:
      operationId: updateDynamics
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X PUT https://api.bithuman.ai/v1/dynamics/A95SXN5716 \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"dynamics": {"enabled": true}, "toggle_enabled": true}'
      summary: Update dynamics configuration
      description: |
        Update gesture settings for an agent. After a successful update,
        movements regeneration is automatically triggered in the background.
      tags: [Dynamics]
      parameters:
        - name: agent_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dynamics]
              properties:
                dynamics:
                  type: object
                  description: |
                    Dynamics configuration to merge with existing data.
                    Fields: `enabled` (boolean), `batch_results` (gesture results),
                    `result` (model path/hash when generated), `talking` (default model path/hash).
                  properties:
                    enabled:
                      type: boolean
                      description: Enable or disable dynamics for this agent.
                    batch_results:
                      type: object
                      description: Map of gesture names to video generation results.
                    result:
                      type: object
                      description: Result model path and hash (set when dynamics generation completes).
                    talking:
                      type: object
                      description: Default talking model path and hash (used when dynamics are disabled).
                toggle_enabled:
                  type: boolean
                  description: |
                    `true` to switch agent to dynamics model, `false` to restore default talking model.
      responses:
        "200":
          description: Dynamics updated and movements regeneration triggered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  message:
                    type: string
                    example: "Dynamics updated successfully and movements regeneration started"
                  agent_id:
                    type: string
                  regeneration_status:
                    type: string
                    enum: [started, failed]
                    description: Whether background movements regeneration was triggered.
                  regeneration_error:
                    type: string
                    description: Error message if regeneration_status is "failed".
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/video/generate:
    post:
      operationId: generateTalkingVideo
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/video/generate \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{
                "model": "essence-2",
                "agent_code": "A95SXN5716",
                "input": {"type": "audio", "audio_url": "https://docs.bithuman.ai/samples/speech-16k-short.wav"}
              }'
            # → {"success": true, "job_id": "vid_3f9a2c1b8e7d4a6f0b21", "status": "processing"}
            # add "wait": true to block (≤ ~90 s) for the finished video_url
      summary: Generate a talking video
      description: |
        Render a talking-video (mp4) from **your audio**, driven by one of
        your agents.

        **Two modes:**
        - **Async (default)** — returns immediately with a `job_id`; poll
          [`GET /v1/video/{job_id}`](#operation/getTalkingVideo) for the finished
          video. Use this for anything but the shortest clips — renders can take a
          while, so you don't want to hold the connection.
        - **Sync** — set `wait: true` (or `?wait=true`). The call blocks until the
          render finishes (up to ~90s) and returns the `video_url` directly in this
          response. If the render is still running at that cap you get the async
          response (`status: processing`) instead — just poll the `job_id`.

        **Input:** your own audio. `audio_url` (a hosted WAV/MP3: a recording, or
        the output of any text-to-speech tool) drives the render directly.

        The output is a public CDN URL. `essence-2` renders at
        **1080p** by default (1080×1920 portrait or 1920×1080 landscape, matching
        the source orientation and capped at the source's long side);
        `expression-2` renders at its native 416×720. Limits: max **120 seconds**
        of output.
      tags: [Video]
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string }
          description: Optional. A unique client-generated key (e.g. a UUID). Retrying with the same key returns the original response instead of starting a second billed job. Keys are retained for 24 hours.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model, input, agent_code]
              properties:
                model:
                  type: string
                  enum: [essence-1, expression-1, essence-2, expression-2]
                  description: |
                    Avatar engine. `essence-1` (2 credits/min) and
                    `expression-1` (4 credits/min) are the classic v1 engines,
                    available today. `expression-2` (4 credits/min) and `essence-2`
                    (4 credits/min) are the v2 engines.
                  example: "essence-2"
                agent_code:
                  type: string
                  description: An agent you own, or a public sample from `GET /v1/models/showcase` (audio input, a model in its `supported_models`) — provides the avatar identity.
                  example: "A95SXN5716"
                wait:
                  type: boolean
                  default: false
                  description: >-
                    Sync toggle. `false` (default) returns a `job_id` immediately
                    (async — poll for the result). `true` blocks until the render
                    finishes (up to ~90s) and returns the finished `video_url` in
                    this response; if it exceeds the cap you get
                    `status: processing` + a `job_id` to poll. `?wait=true` as a
                    query parameter also works.
                input:
                  type: object
                  required: [type]
                  properties:
                    type:
                      type: string
                      enum: [audio]
                    audio_url:
                      type: string
                      description: A public URL to a WAV or MP3 file.
                      example: "https://docs.bithuman.ai/samples/speech-16k-short.wav"
      responses:
        "200":
          description: >-
            Async (or sync-timed-out): `status: processing` + a `job_id` to poll.
            Sync completed: `status: completed` with `video_url`, `duration_seconds`,
            and `credits_charged`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                    example: "vid_3f9a2c1b8e7d4a6f0b21"
                  status:
                    type: string
                    enum: [processing, completed]
                    example: "processing"
                  video_url:
                    type: string
                    description: Public mp4 URL — present only on a completed sync render (wait=true).
                  duration_seconds:
                    type: number
                    format: float
                    description: Output duration — present on a completed sync render.
                  credits_charged:
                    type: integer
                    description: Credits charged — present on a completed sync render.
        "400":
          description: Validation error — invalid model, missing input, or text/audio missing.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Insufficient credits — INSUFFICIENT_BALANCE. The cost is `ceil(minutes) × rate` (essence-1 = 2, expression-1 = 4, expression-2 = 4, essence-2 = 4 credits/min).
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: >-
            `MODEL_NOT_GENERATED` — checked **before any charge**: the
            requested `model` is a family this agent can't be rendered as
            right now. For `expression-2` / `essence-2` that means the trained
            per-identity model hasn't been generated yet (message: `agent
            <code>'s <family> model hasn't been generated yet`, spelled with
            the public family name); `essence-1` needs the agent's `.imx` model file
            (present on every completed essence-1 creation) and
            `expression-1` needs an expression-1 agent (or the free
            post-generation expression-1 model add) with an image.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_NOT_GENERATED"
                  message: "agent A56ZFX6217's essence-2 model hasn't been generated yet"
                  httpStatus: 409
                status: "error"
                status_code: 409
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          description: >-
            `MODEL_NOT_YET_AVAILABLE` — the requested model can't render
            right now (no routeable worker, or the model is temporarily
            paused). All four avatar models render talking video in normal
            operation, so this is a rare, transient condition. Returned
            **before any charge** — retry shortly, or render with another
            of the agent's supported models.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: "MODEL_NOT_YET_AVAILABLE"
                  message: "essence-2 talking video isn't available right now — retry shortly"
                  httpStatus: 503
                status: "error"
                status_code: 503
        "504":
          $ref: "#/components/responses/EdgeGatewayTimeout"

  /v1/video/{job_id}:
    get:
      operationId: getTalkingVideo
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/video/vid_3f9a2c1b8e7d4a6f0b21 \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # poll until status is "completed" — then video_url is the public mp4
      summary: Get talking-video status
      description: |
        Poll a talking-video render job. While rendering, `status` is `processing`.
        When done, `status` is `completed` and the response carries `video_url`,
        `duration_seconds`, and `credits_charged`. On failure, `status` is `failed`
        and the up-front charge is automatically refunded.
      tags: [Video]
      parameters:
        - name: job_id
          in: path
          required: true
          schema:
            type: string
          example: "vid_3f9a2c1b8e7d4a6f0b21"
      responses:
        "200":
          description: Job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  job_id:
                    type: string
                  status:
                    type: string
                    enum: [processing, completed, failed]
                  model:
                    type: string
                  video_url:
                    type: string
                    description: Public mp4 URL (present when status is `completed`).
                  duration_seconds:
                    type: number
                    format: float
                    description: Output duration in seconds (present when `completed`).
                  credits_charged:
                    type: integer
                    description: Credits charged for this render (present when `completed`).
                  error:
                    type: object
                    description: Failure detail (present when status is `failed`).
                    properties:
                      message:
                        type: string
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/agents:
    get:
      operationId: listAgents
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v1/agents?limit=20&status=ready" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List agents
      description: |
        List the agents owned by your API secret, newest first. Paginated.

        Each item is a **summary**, not the full agent record: it carries the
        identity, the state and the launch surface, and nothing else. The
        prompt, the progress fields, the media URLs and the model handle are
        only on [`GET /v1/agent/{code}`](#operation/getAgent) — a caller that
        reads them from a list item reads fields that are not there.

        **Deleted agents are not listed and not counted.** Every page holds
        `limit` items except the last. `pagination.total` is the number of
        agents you reach by paging, and it is the same on every page. Pass
        `?status=ready` if you want only agents that finished generating.
      tags: [Agent Management]
      security:
        - apiSecret: []
      parameters:
        - name: limit
          in: query
          description: Page size (1–100).
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
        - name: offset
          in: query
          description: Number of agents to skip.
          schema: { type: integer, default: 0, minimum: 0 }
        - name: status
          in: query
          description: Filter by generation state (e.g. `ready`, `processing`, `failed`).
          schema: { type: string }
      responses:
        "200":
          description: A page of agents
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/AgentListItem" }
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "502":
          $ref: "#/components/responses/EdgeBadGateway"

  /v1/usage:
    get:
      operationId: getUsage
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v1/usage?limit=50&start=2026-06-01T00:00:00Z" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Usage history
      description: |
        Return your account's usage / activity history, newest first. Each row
        is a metered event. `credits_change` is the signed credit delta — usage
        events are recorded as **positive** credits consumed.
      tags: [Billing]
      security:
        - apiSecret: []
      parameters:
        - name: limit
          in: query
          description: Page size (1–200).
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
        - name: offset
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
        - name: start
          in: query
          description: ISO-8601 timestamp; only return events at/after this time.
          schema: { type: string, format: date-time }
        - name: end
          in: query
          description: ISO-8601 timestamp; only return events at/before this time.
          schema: { type: string, format: date-time }
        - name: agent_code
          in: query
          description: Only return events for this agent.
          schema: { type: string }
      responses:
        "200":
          description: A page of usage events
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/UsageEvent" }
                  pagination:
                    $ref: "#/components/schemas/Pagination"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/webhooks:
    post:
      operationId: createWebhook
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/webhooks \
              -H "api-secret: $BITHUMAN_API_SECRET" \
              -H "content-type: application/json" \
              -d '{"url": "https://example.com/bithuman/webhook", "events": ["agent.ready", "agent.failed"]}'
            # → data.secret is returned ONCE — store it to verify signatures
      summary: Create a webhook
      description: |
        Register an HTTPS endpoint to receive signed event notifications. The
        response includes a one-time `secret` used to verify the
        `X-BitHuman-Signature` header — store it; it is never returned again.
      tags: [Webhooks]
      security:
        - apiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  description: HTTPS endpoint to deliver events to.
                  example: "https://example.com/bithuman/webhook"
                events:
                  type: array
                  description: Event types to subscribe to. Omit or pass `[]` to receive all.
                  items:
                    type: string
                    enum: [agent.ready, agent.failed, video.completed, video.failed]
                  example: ["agent.ready", "agent.failed"]
                description:
                  type: string
                  description: Optional human-readable label.
      responses:
        "200":
          description: Webhook created (includes the one-time signing secret)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    $ref: "#/components/schemas/WebhookWithSecret"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    get:
      operationId: listWebhooks
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/webhooks \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List webhooks
      description: List your registered webhooks (signing secrets are redacted).
      tags: [Webhooks]
      security:
        - apiSecret: []
      responses:
        "200":
          description: Your webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Webhook" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/webhooks/{webhook_id}:
    get:
      operationId: getWebhook
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182 \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get a webhook
      tags: [Webhooks]
      security:
        - apiSecret: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: The webhook (secret redacted)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data: { $ref: "#/components/schemas/Webhook" }
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      operationId: deleteWebhook
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X DELETE https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182 \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Delete a webhook
      tags: [Webhooks]
      security:
        - apiSecret: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  deleted: { type: boolean, example: true }
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/webhooks/{webhook_id}/test:
    post:
      operationId: testWebhook
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/test \
              -H "api-secret: $BITHUMAN_API_SECRET"
            # delivers a one-off "ping" event to your endpoint
      summary: Send a test event
      description: Deliver a one-off `ping` event to verify the endpoint is reachable.
      tags: [Webhooks]
      security:
        - apiSecret: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Delivery attempted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      delivered: { type: boolean, example: true }
                      status_code: { type: integer, example: 200 }
                      attempts: { type: integer, example: 1 }
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/webhooks/{webhook_id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/deliveries \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List delivery attempts
      description: Recent delivery attempts for a webhook (most recent first).
      tags: [Webhooks]
      security:
        - apiSecret: []
      parameters:
        - name: webhook_id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Delivery log
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/WebhookDelivery" }
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ── Self-hosting: the two routes a self-hosted runtime calls ──
  /v1/auth/validate:
    post:
      operationId: validateSelfHostedKey
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/auth/validate \
              -H "Authorization: Bearer $BITHUMAN_API_SECRET" \
              -H "Content-Type: application/json" \
              -d '{"billing_type": "self-hosted-essence-2-model"}'
            # → {"user_id": "...", "key_id": "key_1a2b3c4d5e6f7a8b"}
      summary: Live key check for a self-hosted runtime
      description: |
        The bitHuman SDKs, the CLI and the plugins call this for you when a
        self-hosted session starts and while it runs; you call it only if you
        build your own runtime. Deleting a key in the dashboard stops a running
        self-hosted session at its next check.

        A runtime treats the three answers differently:

        | status | meaning | what a runtime should do |
        |---|---|---|
        | `200` | valid | serve |
        | `401` | **definite**: unknown, revoked or disabled | stop |
        | `503` | we cannot tell right now | retry, keep serving |

        A `503` is never a statement about your key: keep serving and retry.
      tags: [Self-hosting]
      security:
        - bearerApiSecret: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                billing_type:
                  type: string
                  description: |
                    The self-hosted product this runtime serves. Always send
                    the key for the product you run.
                  example: "self-hosted-essence-2-model"
      responses:
        "200":
          description: The key is valid.
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: string
                    description: |
                      The account this key bills to. A response with no
                      `user_id` must be treated as a failure — an
                      identity-less "ok" cannot be billed to anybody.
                  key_id:
                    type: string
                    example: "key_1a2b3c4d5e6f7a8b"
        "401":
          description: Definite — unknown, revoked or disabled key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SelfHostError" }
        "429":
          $ref: "#/components/responses/RateLimitedFailedAuth"
        "503":
          description: |
            We cannot validate right now. Retry (a `Retry-After` header is
            sent) and keep serving — this is not a statement about your key.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SelfHostError" }

  /v1/meter/beats:
    post:
      operationId: postMeterBeat
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/meter/beats \
              -H "Authorization: Bearer $BITHUMAN_API_SECRET" \
              -H "Content-Type: application/json" \
              -d '{"session_id":"s-1","install_id":"i-1","seq":1,
                   "served_s":60,"billing_type":"self-hosted-essence-2-model"}'
      summary: Report usage from a self-hosted session
      description: |
        The bitHuman SDKs, the CLI and the plugins call this for you: a
        self-hosted or on-device session reports its running time here, and
        that is what bills it ([Pricing](https://docs.bithuman.ai/pricing)).
        You call it only if you build your own runtime.

        A report covers the seconds the session ran, idle included, not frames
        rendered. The product (`billing_type`) is fixed by the first report of a
        session.

        Read **only the status code**:

        | status | meaning | what a runtime should do |
        |---|---|---|
        | `2xx` | the interval is recorded | count it as reported |
        | `402` | balance depleted / account suspended | **keep rendering**, report it again later |
        | `401` | definite bad key | stop |
        | `503` | we cannot tell right now | retry |

        A `402` never stops a running session; the next session is refused
        until you top up. A retried report (same `session_id`, `install_id`
        and `seq`) is never charged twice.
      tags: [Self-hosting]
      security:
        - bearerApiSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [session_id, seq, served_s]
              properties:
                session_id:
                  type: string
                  maxLength: 128
                  description: Stable id for the session being billed.
                install_id:
                  type: string
                  maxLength: 128
                  description: Stable id for this installation.
                seq:
                  type: integer
                  minimum: 1
                  description: Beat sequence number within the session.
                served_s:
                  type: number
                  description: |
                    Seconds of session covered by this beat. Clamped
                    server-side; wall-clock, idle included.
                billing_type:
                  type: string
                  description: |
                    The self-hosted product wire key. Always send the key
                    for the product you run.
                  example: "self-hosted-essence-2-model"
      responses:
        "200":
          description: >-
            The interval is durably recorded. Advance the meter clock. Until
            2026-10-12, a Free account's answer also carries `plan_notice` and
            the `X-Bithuman-Plan-Notice` header; show it as sent.
          headers:
            X-Bithuman-Plan-Notice:
              schema:
                type: string
              description: >-
                The same text as `plan_notice`: "Free-plan API and SDK access
                ends on 2026-10-12. Upgrade at https://www.bithuman.ai/pricing
                to keep it.", or, for a Free account keeping access on top-up
                credits bought before 2026-09-27, the wording that says so.
        "400":
          description: Malformed beat (`session_id` and a positive `seq` are required).
        "401":
          description: Definite — unknown, revoked or disabled key.
        "402":
          description: |
            Balance depleted (`error.type` `insufficient_balance`, with a
            `topup_url`) or account suspended (`account_suspended`). Do **not**
            advance the meter clock; keep rendering.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SelfHostError"
              example:
                accepted: false
                error:
                  type: "insufficient_balance"
                  message: "credit balance depleted; top up to continue metered service"
                  topup_url: "https://www.bithuman.ai/billing#credits"
        "403":
          description: |
            `PLAN_REQUIRED` (`error.type` `plan_required`): from 2026-10-12, a
            Free account's API secret, with an `upgrade_url`; or a product your
            plan does not include.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SelfHostError"
        "429":
          $ref: "#/components/responses/RateLimitedFailedAuth"
        "503":
          description: We cannot record right now. Retry.

  /v2/{user_id}/api-secrets:
    parameters:
      - $ref: "#/components/parameters/UserId"
    post:
      operationId: createApiSecret
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST "https://api.bithuman.ai/v2/$USER_ID/api-secrets" \
              -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
              -d '{"alias":"prod-server"}'
      summary: Create an API secret
      description: |
        Creates an API secret on your account. The response is the **only**
        one that returns the full secret: store it now. Send `{}` for an
        auto-generated alias; a request with no body returns `400`.
      tags: [API secrets]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                alias:
                  type: string
                  description: A label for the API secret. Auto-generated (a timestamp) if omitted.
                  example: "prod-server"
      responses:
        "200":
          description: The new secret, returned once.
          content:
            application/json:
              example: { "alias": "prod-server", "secret": "k7m2p9x4…Sn3Q8vT1w…aC8e" }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: "`FORBIDDEN` — the `user_id` in the path is not yours."
        "404":
          description: Account not found.
        "409":
          description: An API secret with this alias already exists.
    get:
      operationId: listApiSecrets
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v2/$USER_ID/api-secrets" -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List API secrets
      description: |
        Your API secrets, masked. The raw secret is never returned here, only
        `key_display`. `name`, `key` and `alias` all carry the alias label.
      tags: [API secrets]
      responses:
        "200":
          description: The masked list.
          content:
            application/json:
              example:
                user_id: "8f14e45f-…"
                data:
                  - { "alias": "prod-server", "name": "prod-server", "key_display": "k7m2p9x4••••••••••••••••••••aC8e", "created_at": "2026-07-15T15:30:45Z" }
                total_count: 1
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: "`FORBIDDEN` — the `user_id` in the path is not yours."

  /v2/{user_id}/api-secrets/{alias}/get-value:
    parameters:
      - $ref: "#/components/parameters/UserId"
      - name: alias
        in: path
        required: true
        schema: { type: string }
        description: The API secret's alias.
    get:
      operationId: revealApiSecret
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v2/$USER_ID/api-secrets/prod-server/get-value" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Reveal an API secret (console only)
      description: |
        Console-only: only the signed-in owner can reveal a stored secret, in
        the console under Developer → API Secrets. Called with an API secret
        it always returns `403 SECRET_REVEAL_CONSOLE_ONLY`, so one leaked
        secret cannot read your others.
      tags: [API secrets]
      responses:
        "403":
          description: "`SECRET_REVEAL_CONSOLE_ONLY` — reveal it in the console, or create a new secret."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /v2/{user_id}/api-secrets/{alias}:
    parameters:
      - $ref: "#/components/parameters/UserId"
      - name: alias
        in: path
        required: true
        schema: { type: string }
        description: The API secret's alias.
    delete:
      operationId: deleteApiSecret
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X DELETE "https://api.bithuman.ai/v2/$USER_ID/api-secrets/prod-server" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Delete an API secret
      description: |
        Removes an API secret and revokes it at the runtime at once. Only that
        secret stops working; to stop every secret, use
        `POST /v2/{user_id}/runtime/revoke-all`.
      tags: [API secrets]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              example: { "alias": "prod-server", "message": "API secret deleted successfully" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No API secret with that alias.

  /v2/{user_id}/providers:
    parameters:
      - $ref: "#/components/parameters/UserId"
    get:
      operationId: getProviders
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v2/$USER_ID/providers" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Get provider config
      description: |
        Your configured provider keys (BYOK). Sensitive keys are returned only
        as `{present, hint, updated_at}`, never in full.
      tags: [Providers]
      responses:
        "200":
          description: The masked provider list.
          content:
            application/json:
              example:
                user_id: "3f9a…"
                providers:
                  entries:
                    - id: "b7e1…"
                      platform: "openai"
                      label: "OpenAI"
                      options: {}
                      secrets:
                        apiKey: { "present": true, "hint": "sk…4f2a", "updated_at": "2026-07-02T09:30:00Z" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Account not found.
    put:
      operationId: updateProviders
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X PUT "https://api.bithuman.ai/v2/$USER_ID/providers" \
              -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
              -d '{"entries":[{"platform":"openai","label":"OpenAI","credentials":{"apiKey":"sk-…"}}]}'
      summary: Update provider config
      description: |
        Replaces your provider list: `entries` is the **whole** list, and
        `{"entries": []}` clears every provider. Existing entries are matched by
        `id` and merged (unspecified credentials are kept); new entries get a
        generated id. Returns the same masked shape as `GET`.
      tags: [Providers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entries]
              properties:
                entries:
                  type: array
                  items:
                    type: object
                    required: [platform]
                    properties:
                      platform: { type: string, description: "Provider id, e.g. `openai`, `elevenlabs`, `cartesia`, `grok`." }
                      id: { type: string, description: Existing entry id to update; omit to add a new one. }
                      label: { type: string, description: Display name; defaults to the platform name. }
                      credentials: { type: object, description: Key/value credentials. Sensitive keys are encrypted at rest. }
                      options: { type: object, description: Per-capability configuration. }
                      clearSecrets: { type: array, items: { type: string }, description: Sensitive credential keys to remove. }
                      metadata: { type: object, description: Freeform metadata. }
      responses:
        "200":
          description: The stored list, masked as in `GET`.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Account not found.
        "500":
          description: A storage failure.

  /v2/{user_id}/runtime-sessions:
    parameters:
      - $ref: "#/components/parameters/UserId"
    get:
      operationId: listRuntimeSessions
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v2/$USER_ID/runtime-sessions?window=live" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: List sessions
      description: |
        Your sessions plus live account KPIs (`active_count`,
        `live_burn_rate_cr_per_min`, `credits_this_hour`, account-wide over the
        last hour). A transient `500 UPSTREAM_ERROR` can be retried.
      tags: [Runtime sessions]
      parameters:
        - name: window
          in: query
          schema: { type: string, enum: [all, live, recent], default: all }
          description: "`live` (only running), `recent` (idle + ended), or `all`."
        - name: kind
          in: query
          schema: { type: string, enum: [all, conversations, self_hosted], default: all }
          description: "`conversations` (cloud), `self_hosted`, or `all`."
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
          description: Max sessions to return.
      responses:
        "200":
          description: KPIs and the filtered session list.
          content:
            application/json:
              example:
                success: true
                data:
                  active_count: 1
                  live_burn_rate_cr_per_min: 4
                  credits_this_hour: 52
                  sessions:
                    - { "id": "a3f1c8e2-…-9b02", "agent_code": "agent_greeter", "agent_name": "Greeter", "billing_type": "usage_essence_2_model_cloud", "key_alias": "prod-server", "room_name": "room_a3f1c8e2", "started": "2026-07-15T15:20:00Z", "last_seen": "2026-07-15T15:33:00Z", "minutes": 13.0, "credits": 52, "burn_rate_cr_per_min": 4, "status": "live", "has_transcript": true }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: "`FORBIDDEN` — the `user_id` in the path is not yours."

  /v2/{user_id}/runtime-sessions/{session_id}/messages:
    parameters:
      - $ref: "#/components/parameters/UserId"
      - $ref: "#/components/parameters/SessionId"
    get:
      operationId: getRuntimeSessionMessages
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v2/$USER_ID/runtime-sessions/a3f1c8e2-…/messages" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Read a transcript
      description: |
        The conversation, oldest first. Only cloud voice and chat sessions
        record a transcript; self-hosted sessions return `messages: []`.
      tags: [Runtime sessions]
      responses:
        "200":
          description: The transcript.
          content:
            application/json:
              example:
                success: true
                data:
                  session_id: "a3f1c8e2-…"
                  agent_code: "agent_greeter"
                  started: "2026-07-15T15:20:00Z"
                  ended: "2026-07-15T15:33:00Z"
                  live: true
                  messages:
                    - { "timestamp": "2026-07-15T15:20:03Z", "role": "user", "message": "Hi there" }
                    - { "timestamp": "2026-07-15T15:20:05Z", "role": "assistant", "message": "Hello! How can I help?" }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v2/{user_id}/runtime-sessions/{session_id}/terminate:
    parameters:
      - $ref: "#/components/parameters/UserId"
      - $ref: "#/components/parameters/SessionId"
    post:
      operationId: terminateRuntimeSession
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST "https://api.bithuman.ai/v2/$USER_ID/runtime-sessions/a3f1c8e2-…/terminate" \
              -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" -d '{}'
      summary: Terminate a session
      description: |
        Stops **one** session; never touches an API secret. The session stops
        counting toward your plan's concurrent sessions at once.
      tags: [Runtime sessions]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Free-text stop reason.
                  default: user-terminated
      responses:
        "200":
          description: Terminated.
          content:
            application/json:
              example:
                success: true
                data: { "session_id": "a3f1c8e2-…", "closed": true, "ended": true, "self_hosted": false, "note": "Session ended — the avatar disconnected and the conversation closed." }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No such session (or not yours).

  /v2/{user_id}/runtime/revoke-all:
    parameters:
      - $ref: "#/components/parameters/UserId"
    post:
      operationId: revokeAllRuntimeKeys
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST "https://api.bithuman.ai/v2/$USER_ID/runtime/revoke-all" \
              -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" -d '{}'
      summary: Revoke all runtime keys
      description: |
        The emergency stop: disables **every** runtime key on the account. Live
        sessions stop within one refresh cycle (about 5 minutes). It does not
        delete your API secrets; creating a new API secret clears the
        suspension.
      tags: [Runtime sessions]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  description: Free-text reason.
      responses:
        "200":
          description: Revoked.
          content:
            application/json:
              example:
                success: true
                data: { "revoked": true, "effective_in_seconds": 320, "runtime_suspended": true, "note": "All runtime keys disabled. Creating a new API key restores runtime access." }
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/runtime-sessions/{session_id}:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    get:
      operationId: getCloudAvatarSession
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl "https://api.bithuman.ai/v1/runtime-sessions/$SESSION_ID" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: Read a cloud avatar session
      description: |
        A [cloud avatar](https://docs.bithuman.ai/platforms/livekit/cloud-avatar)
        session you started: `status` (`starting`, `live` or `ended`), times,
        `billed_seconds` and `credits` so far.
      tags: [Runtime sessions]
      responses:
        "200":
          description: The session.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No such session (or not yours).

  /v1/runtime-sessions/{session_id}/end:
    parameters:
      - $ref: "#/components/parameters/SessionId"
    post:
      operationId: endCloudAvatarSession
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST "https://api.bithuman.ai/v1/runtime-sessions/$SESSION_ID/end" \
              -H "api-secret: $BITHUMAN_API_SECRET"
      summary: End a cloud avatar session
      description: |
        Ends a [cloud avatar](https://docs.bithuman.ai/platforms/livekit/cloud-avatar)
        you started, by the `session_id` its start returned. No body. Billing
        stops at this call and the concurrent-session slot frees at once.
        Calling it again returns the same answer and bills nothing.
      tags: [Runtime sessions]
      responses:
        "200":
          description: Ended. When `ended` is `false`, `note` says when the avatar leaves the room.
          content:
            application/json:
              example: { "session_id": "cs_4f1c…", "status": "ended", "agent_id": "A23KSG5258", "model": "essence-2", "room_name": "my-room", "started_at": "…", "ended_at": "…", "billed_seconds": 131.2, "credits": 9, "ended": true, "slot_released": true, "note": "Session ended: the avatar left the room and billing stopped." }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: No such session (or not yours).

  /v1/knowledge/files:
    post:
      operationId: ingestKnowledgeFile
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/knowledge/files \
              -H "api-secret: $ORG_API_SECRET" -H "content-type: application/json" \
              -d '{"url":"https://example.com/help","name":"Help Center","sync_freq":"weekly"}'
      summary: Ingest a file or URL
      description: |
        Adds one source to your organization: an uploaded file
        (`multipart/form-data`, fields `file` up to 100 MB and `name`) or a URL
        to fetch (JSON). Returns the file row at status `uploaded`; conversion
        runs in the background. Needs an organization API secret; a personal
        secret returns `403`.
      tags: [Knowledge]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, description: "A public `http(s)` URL to fetch and convert." }
                name: { type: string, description: Display name; defaults to the URL host. }
                sync_freq: { type: string, enum: [never, daily, weekly, monthly], default: never, description: Re-sync cadence. }
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: The document bytes. Max 100 MB. }
                name: { type: string, description: Display name; defaults to the uploaded filename. }
      responses:
        "201":
          description: The file row.
          content:
            application/json:
              example: { "id": "3f2b9c14-8a7e-4d21-9f10-2c5b6e0a11dd", "org_id": "org_9d1f", "file_name": "handbook.pdf", "ext": "pdf", "size_bytes": 428193, "status": "uploaded", "source_kind": "upload", "created_at": "2026-07-15T12:00:00Z" }
        "403":
          description: A personal API secret; use an organization API secret.
        "413":
          description: File over 100 MB.
        "422":
          description: Empty file, missing `file`/`url`, non-public URL, or bad `sync_freq`.

  /v1/knowledge/kbs:
    post:
      operationId: createKnowledgeBase
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/knowledge/kbs \
              -H "api-secret: $ORG_API_SECRET" -H "content-type: application/json" \
              -d '{"name":"Support KB","file_ids":["3f2b9c14-8a7e-4d21-9f10-2c5b6e0a11dd"]}'
      summary: Create a knowledge base
      description: |
        Creates a knowledge base and, by default, starts its first build.
        `build_dispatched` is `true` only when `build` was `true`, files were
        attached and the build queued.
      tags: [Knowledge]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, description: KB name (1–120 chars). }
                description: { type: string, description: Up to 2000 chars. }
                file_ids: { type: array, items: { type: string }, description: "File `id`s to attach (your organization's own files)." }
                build: { type: boolean, default: true, description: Build immediately after create. }
      responses:
        "201":
          description: The KB row, plus `file_ids` and `build_dispatched`.
          content:
            application/json:
              example: { "id": "b71e0a92-4c3d-4f88-8a01-9e2f4c6d7b10", "org_id": "org_9d1f", "name": "Support KB", "status": "stale", "content_rev": 1, "file_ids": ["3f2b9c14-8a7e-4d21-9f10-2c5b6e0a11dd"], "build_dispatched": true }
        "403":
          description: "A `file_id` isn't yours, or a personal API secret."
        "404":
          description: "A `file_id` doesn't exist."
    get:
      operationId: listKnowledgeBases
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/v1/knowledge/kbs -H "api-secret: $ORG_API_SECRET"
      summary: List knowledge bases
      description: The organization's knowledge bases, newest first, each with `agent_count` and `source_count`.
      tags: [Knowledge]
      responses:
        "200":
          description: The list.
          content:
            application/json:
              example:
                kbs:
                  - { "id": "b71e0a92-4c3d-4f88-8a01-9e2f4c6d7b10", "name": "Support KB", "status": "ready", "content_rev": 2, "built_rev": 2, "artifact_url": "https://…/artifact.zip", "agent_count": 1, "source_count": 3 }
                total: 1

  /v1/knowledge/kbs/{kb_id}/rebuild:
    parameters:
      - name: kb_id
        in: path
        required: true
        schema: { type: string }
        description: The knowledge base id.
    post:
      operationId: rebuildKnowledgeBase
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/knowledge/kbs/b71e0a92-…/rebuild \
              -H "api-secret: $ORG_API_SECRET"
      summary: Rebuild a knowledge base
      description: |
        Re-indexes a knowledge base after its sources change. Returns the
        queued build row; indexing runs in the background. Builds run one at a
        time per account, up to 20 per day.
      tags: [Knowledge]
      responses:
        "200":
          description: The queued build.
          content:
            application/json:
              example: { "id": "e5c9a1f7-0b44-4a6e-9c22-77d0b3e18a55", "kb_id": "b71e0a92-4c3d-4f88-8a01-9e2f4c6d7b10", "status": "queued", "started_at": "2026-07-15T12:10:00Z" }
        "404":
          description: Unknown knowledge base.
        "409":
          description: "`BUILD_IN_PROGRESS`, `BUILD_CONCURRENCY` or `BUILD_DAILY_CAP`."
        "422":
          description: "`KB_EMPTY` — no source files."

  /v1/realtime/connect:
    post:
      operationId: realtimeConnect
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl -X POST https://api.bithuman.ai/v1/realtime/connect \
              -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
              -d '{"action": "hangup", "call": "<X-Bithuman-Call>"}'
      summary: Start or end a WebRTC Realtime call
      description: |
        Starts a WebRTC voice call: send the SDP offer as the body
        (`Content-Type: application/sdp`) with `?model=gpt-realtime-mini`; a
        `201` carries the SDP answer, and the `X-Bithuman-Call` header names
        the call. End it with a JSON body `{"action": "hangup", "call": …}` —
        billing stops when you hang up. See
        [Realtime](https://docs.bithuman.ai/api/realtime#webrtc).
      tags: [Realtime]
      parameters:
        - name: model
          in: query
          schema: { type: string, example: gpt-realtime-mini }
          description: The Realtime model, fixed for the call.
      requestBody:
        required: true
        content:
          application/sdp:
            schema: { type: string, description: The SDP offer. }
          application/json:
            schema:
              type: object
              properties:
                action: { type: string, enum: [hangup] }
                call: { type: string, description: "The `X-Bithuman-Call` value from the start." }
      responses:
        "201":
          description: The SDP answer (`application/sdp`).
        "401":
          $ref: "#/components/responses/Unauthorized"

  /readyz:
    get:
      operationId: getReadiness
      x-codeSamples:
        - lang: shell
          label: cURL
          source: |
            curl https://api.bithuman.ai/readyz
            # → {"status":"ok","families":{"sessions":{"status":"ok"}, ...}}
      summary: API readiness
      description: |
        Whether the API is refusing work right now. No credential required.
        It always answers HTTP 200: read the `status` field (`ok`, `degraded`
        or `unhealthy`). For the overall service status, use
        [status.bithuman.ai](https://status.bithuman.ai).
      tags: [Status]
      security: []
      responses:
        "200":
          description: The readiness verdict.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok, degraded, unhealthy]
                  window_seconds:
                    type: number
                    example: 900
                  observed_seconds:
                    type: number
                  replica_scope:
                    type: string
                    example: "this process only — not a fleet view"
                  families:
                    type: object
                    additionalProperties:
                      type: object
                      properties:
                        status:
                          type: string
                          enum: [ok, degraded, unhealthy]

components:
  securitySchemes:
    apiSecret:
      type: apiKey
      name: api-secret
      in: header
      description: |
        Your bitHuman API secret. Get one at
        [www.bithuman.ai/developer/api-keys](https://www.bithuman.ai/developer/api-keys).
    bearerApiSecret:
      type: http
      scheme: bearer
      description: |
        The same API secret, sent as `Authorization: Bearer <api-secret>`. Self-
        hosted runtimes send it this way; the classic `api-secret` header works
        on the same routes.

  parameters:
    UserId:
      name: user_id
      in: path
      required: true
      schema: { type: string }
      description: "Your own account id, from `GET /v1/me` (`data.user_id`). Another account's id returns `403 FORBIDDEN`."
    SessionId:
      name: session_id
      in: path
      required: true
      schema: { type: string }
      description: The session id.
  schemas:
    SelfHostError:
      type: object
      description: |
        The error envelope the self-hosting routes return. `error.type` is the
        machine-readable word a runtime switches on; `error.code`,
        `error.httpStatus` and `error.message` are the platform envelope every
        other endpoint uses. Both are present on purpose — the self-hosted
        container's key validator reads `type`, and the additive `code` means a
        developer switching on `error.code` no longer gets `undefined` from
        these routes.
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              example: "missing_credentials"
            code:
              type: string
              example: "MISSING_AUTH"
            httpStatus:
              type: integer
              example: 401
            message:
              type: string
            upgrade_url:
              type: string
              description: On a `plan_required` refusal (403), where to choose a plan.
              example: "https://www.bithuman.ai/pricing"
            topup_url:
              type: string
              description: On an `insufficient_balance` refusal (402), where to top up credits.
              example: "https://www.bithuman.ai/billing#credits"
    ProviderSelection:
      description: >-
        Which provider one capability uses. `"default"` is the
        platform's own provider; the object form points the capability at an
        entry you registered with `PUT /v2/{user_id}/providers`.
      oneOf:
        - type: string
          enum: [default]
        - type: object
          required: [mode]
          properties:
            mode:
              type: string
              enum: [default, custom]
            provider_id:
              type: string
              description: >-
                Required when `mode` is `custom`: the `id` of one of your
                registered provider entries. `providerId` is accepted as a
                spelling of the same field. An id this account has not
                registered is refused `400 VALIDATION_ERROR`.
              example: "43c9a2a3-8664-4a33-a66d-77490256e7e1"

    ModelStatusEntry:
      type: object
      description: Per-family readiness for one of the agent's models.
      properties:
        state:
          type: string
          description: >-
            `ready` when the agent can be launched as this family, `pending`
            while it is still being prepared.
          example: "ready"
        reason:
          type: string
          nullable: true
          description: Why the family is not ready yet; `null` once it is.
          example: "this model is still being prepared for this agent"
        updated_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            When this family last changed state. `null` for a family that was
            available from the agent's creation and never transitioned.
          example: "2026-09-20T15:50:15.910740+00:00"

    Agent:
      type: object
      description: >-
        An agent's full record, as returned by `GET /v1/agent/{code}` and
        `GET /v1/agent/status/{agent_id}`. Every field below is present on
        every response; fields that do not apply to the agent are `null`
        rather than absent. `GET /v1/agents` returns a shorter summary — see
        `AgentListItem`.
      properties:
        code:
          type: string
          description: The agent's short code — the primary identifier used across the platform and the embed/viewer URLs.
          example: "A95SXN5716"
        agent_id:
          type: string
          description: The same value as `code`, under the name the creation response uses.
          example: "A95SXN5716"
        name:
          type: string
          nullable: true
          description: >-
            The agent's display name. **Generated** from the prompt and
            portrait during the `persona` step — it cannot be supplied at
            creation or changed afterwards, and it is `null` until that step
            completes.
          example: "Fitness Coach"
        description:
          type: string
          nullable: true
          description: Generated one-paragraph description of the agent. `null` until the `persona` step completes.
        language:
          type: string
          nullable: true
          description: Detected/assigned language tag, e.g. `en`.
          example: "en"
        gender:
          type: string
          nullable: true
          example: "masculine"
        model:
          type: string
          nullable: true
          description: The model family the agent was created with.
          example: "expression-2"
        supported_models:
          type: array
          items: { type: string }
          description: >-
            Canonical model families this agent can be launched as right now
            — `essence-1` when its `.imx` exists, and `essence-2` /
            `expression-2` once their
            trained per-identity model has been generated. Tier slugs inherit
            their family and are not listed.
          example: ["essence-1", "essence-2"]
        model_status:
          type: object
          description: >-
            Readiness per model family, keyed by family name. A family is
            listed here before it is launchable, with the reason it is not —
            so a caller can tell "still training" from "never requested".
            `supported_models` is the `ready` subset of these keys.
          additionalProperties:
            $ref: "#/components/schemas/ModelStatusEntry"
          example:
            expression-2: { state: "ready", reason: null, updated_at: "2026-09-20T15:50:15.910740+00:00" }
        subject:
          type: string
          nullable: true
          description: The subject class the creation classifier recorded for the input, when one was run.
        system_prompt:
          type: string
          nullable: true
          example: "You are a friendly fitness coach."
        image_url:
          type: string
          nullable: true
          description: Avatar face image URL.
        image_path:
          type: string
          nullable: true
          description: The stored location of the portrait. Same value as `image_url` for agents whose portrait is public.
        video_url:
          type: string
          nullable: true
          description: >-
            The agent's identity video URL — generated internally at
            creation (10 seconds, authored to loop without a seam). Read-only
            output; video is never a creation input.
        video_path:
          type: string
          nullable: true
          description: The stored location of the identity video. Same value as `video_url` for agents whose video is public.
        model_url:
          type: string
          nullable: true
          description: >-
            Where the model artifact lives. For `essence-1` this is a
            fetchable URL; for the second-generation families it is an
            opaque stored handle, **not** a link you can download. Use
            [the download endpoint](#operation/downloadAgentModel) to get the
            file itself.
        model_path:
          type: string
          nullable: true
          description: The same stored handle as `model_url`, under its storage-side name.
        model_hash:
          type: string
          nullable: true
          description: Content digest of the model artifact, when one has been recorded.
        voice_id:
          type: string
          nullable: true
          description: Voice identifier.
        voice_path:
          type: string
          nullable: true
          description: The same value as `voice_id`, under its storage-side name.
        status:
          type: string
          description: >-
            Current agent state — `processing`, `generating`, `completed`,
            `success` (all intermediate), `ready` (terminal success) or
            `failed` (terminal error). `deleted` for a deleted agent.
          example: "ready"
        event_type:
          type: string
          nullable: true
          description: Most recent pipeline event, e.g. `persona_created`, `voice_created`, `video_created`, `lip_created`.
        progress:
          type: number
          format: float
          nullable: true
          minimum: 0
          maximum: 1
          description: Generation progress as a fraction from 0.0 to 1.0.
        progress_msg:
          type: string
          nullable: true
          description: Human-readable progress description.
        current_step:
          type: string
          nullable: true
          description: >-
            Current generation step. Known values: `payment`, `persona`,
            `voice_image`, `video`, `awaiting_face_marking`, `lip_sync`,
            `done`.
          example: "done"
        error_message:
          type: string
          nullable: true
          description: Error details when `status` is `failed`; `null` otherwise.
        error_type:
          type: string
          nullable: true
          description: Which stage the failure came from; `null` when the agent has not failed.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        dynamics:
          type: object
          nullable: true
          description: Gesture configuration and available gestures. `{}` when none are configured.

    AgentListItem:
      type: object
      description: >-
        One agent as `GET /v1/agents` returns it — a summary, not the full
        record. Every field below is present on every item; the detail fields
        (`system_prompt`, `progress`, `model_url`, the media URLs, …) are
        **not** part of this shape, so read them from
        `GET /v1/agent/{code}` instead of expecting them here.
      properties:
        code:
          type: string
          example: "A95SXN5716"
        agent_id:
          type: string
          description: The same value as `code`.
          example: "A95SXN5716"
        name:
          type: string
          nullable: true
          description: Generated display name; `null` before the persona step and for deleted agents.
          example: "Fitness Coach"
        description:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
          example: "expression-2"
        gender:
          type: string
          nullable: true
        language:
          type: string
          nullable: true
          example: "en"
        image_path:
          type: string
          nullable: true
          description: Portrait URL.
        thumbnail_path:
          type: string
          nullable: true
          description: Thumbnail URL for the portrait.
        status:
          type: string
          description: >-
            Generation state — `processing`, `generating`, `completed`,
            `success`, `ready`, `failed` — or `deleted` for an agent you
            deleted, or `unknown`. **Deleted agents stay in this listing**;
            filter them out with `?status=ready` or on the field.
          example: "ready"
        supported_models:
          type: array
          items: { type: string }
          description: Model families this agent can be launched as right now; `[]` while it is still generating.
          example: ["essence-1", "essence-2"]
        model_status:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/ModelStatusEntry"
          description: Readiness per model family; `{}` while the agent is still generating.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Pagination:
      type: object
      properties:
        limit:
          type: integer
          example: 20
        offset:
          type: integer
          example: 0
        total:
          type: integer
          description: Total matching rows across all pages.
          example: 137
        has_more:
          type: boolean
          example: true

    UsageEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
        activity_type:
          type: string
          example: "bithuman-serve"
        pricing_code:
          type: string
          example: "usage_essence_model_self_hosted"
        app:
          type: string
          example: "my-app"
        agent_code:
          type: [string, "null"]
        credits_change:
          type: number
          description: Signed credit delta; usage events are positive credits consumed.
          example: 1
        frames_rendered:
          type: [integer, "null"]
        room_name:
          type: [string, "null"]
        start_time:
          type: [string, "null"]
          format: date-time
        end_time:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time

    Webhook:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          example: "https://example.com/bithuman/webhook"
        events:
          type: array
          items:
            type: string
          description: Subscribed event types; empty means all.
          example: ["agent.ready", "agent.failed"]
        description:
          type: [string, "null"]
        active:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WebhookWithSecret:
      allOf:
        - $ref: "#/components/schemas/Webhook"
        - type: object
          properties:
            secret:
              type: string
              description: |
                HMAC-SHA256 signing secret, returned **only once** at creation.
                Verify deliveries with
                `sha256 = HMAC(secret, "{X-BitHuman-Timestamp}.{raw_body}")`
                and compare against the `X-BitHuman-Signature` header.
              example: "whsec_60fe3d…"

    WebhookDelivery:
      type: object
      properties:
        id:
          type: string
          format: uuid
        event_type:
          type: string
          example: "agent.ready"
        status_code:
          type: [integer, "null"]
          description: HTTP status returned by your endpoint; null on network error/timeout.
          example: 200
        attempts:
          type: integer
          example: 1
        delivered:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: "UNAUTHORIZED"
            message:
              type: string
              example: "Invalid API secret"
            httpStatus:
              type: integer
              example: 401
            upgrade_url:
              type: string
              description: >-
                On `PLAN_REQUIRED`, `AGENT_LIMIT_REACHED` and
                `CONCURRENCY_LIMIT_REACHED` (403): where to choose a plan. Send
                the user there instead of retrying. See
                [Plan and credit refusals](https://docs.bithuman.ai/api/errors#plan-and-credit-refusals).
              example: "https://www.bithuman.ai/pricing"
            topup_url:
              type: string
              description: >-
                On `INSUFFICIENT_BALANCE` (402): where to top up credits.
              example: "https://www.bithuman.ai/billing#credits"
            details:
              type: object
              description: >-
                Code-specific fields. `CONCURRENCY_LIMIT_REACHED` sends `tier`,
                `cap`, `active` and `upgrade_url`.
              additionalProperties: true
        status:
          type: string
          example: "error"
        status_code:
          type: integer
          example: 401

  responses:
    BadRequest:
      description: Missing required parameter or malformed request. The HTTP status equals `status_code` (400).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "MISSING_PARAM"
              message: "Missing required field: text"
              httpStatus: 400
            status: "error"
            status_code: 400

    Unauthorized:
      description: |
        Authentication failed. `UNAUTHORIZED` when the `api-secret` header is
        present but invalid; `MISSING_AUTH` when it is absent. The HTTP status
        equals `status_code` (401).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "UNAUTHORIZED"
              message: "Invalid API secret"
              httpStatus: 401
            status: "error"
            status_code: 401

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "NOT_FOUND"
              message: "Agent not found"
              httpStatus: 404
            status: "error"
            status_code: 404

    ValidationError:
      description: Body failed schema validation. The HTTP status equals `status_code` (400).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "VALIDATION_ERROR"
              message: "Invalid request parameters"
              httpStatus: 400
            status: "error"
            status_code: 400

    RateLimited:
      description: |
        Too many requests. The limiter is **per account** — every API secret on
        the account draws from one bucket — and **per cost tier**, so a burst of
        reads never uses up your generate budget. Your plan sets the number for
        each tier; the matrix, the tier each route belongs to and a retry
        recipe are on
        [Rate limits](https://docs.bithuman.ai/api/rate-limits).

        Wait `Retry-After` seconds and retry. The `X-RateLimit-*` headers let a
        client slow down *before* it gets here, but header coverage is uneven
        across routes — read them defensively and fall back to exponential
        backoff when they are absent.

        On a route that can also be called without an API secret, an anonymous
        call is metered per network address instead: same body, same
        `Retry-After`, no `X-RateLimit-*`.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
          example: 1
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Requests per minute your plan allows on this request's cost tier.
          example: 30
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Whole requests left in the bucket right now.
          example: 0
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix time at which the bucket is fully refilled.
          example: 1758380460
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "RATE_LIMITED"
              message: "Too many generate requests for this api-secret. Retry in ~1s."
              httpStatus: 429
            status: "error"
            status_code: 429

    RateLimitedByAddress:
      description: |
        Too many requests from this network address. These routes carry their
        own credential and are checked by the service behind them, so there is
        no account to meter at the edge — they get one flat limit per
        address (120 requests/minute) instead of the per-plan cost tiers, and
        no `X-RateLimit-*` headers. Wait `Retry-After` seconds and retry. See
        [Rate limits](https://docs.bithuman.ai/api/rate-limits).
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
          example: 1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: "RATE_LIMITED"
              message: "Too many requests from this address. Retry in ~1s."
              httpStatus: 429
            status: "error"
            status_code: 429

    RateLimitedFailedAuth:
      description: |
        Too many **failed** key validations from this network address — 30 per
        minute. Only failures spend budget, so a runtime presenting a live key
        never reaches this, however often it calls. It says nothing about
        whether your key is valid: wait `Retry-After` seconds, retry, and read
        the answer you get then. See
        [Rate limits](https://docs.bithuman.ai/api/rate-limits).
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.
          example: 1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/SelfHostError"
          example:
            error:
              type: "rate_limited"
              message: "too many failed validations"

    EdgeBadGateway:
      description: >-
        Raised by the content delivery network in front of the API, before the
        request reaches it. **The body is an HTML page, not the JSON envelope
        every other status uses** — check `Content-Type` before parsing a 5xx.
        A `Retry-After` header is present, so an ordinary retry helper still
        works. Any route can answer this.
      content:
        text/html:
          schema:
            type: string
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.

    EdgeGatewayTimeout:
      description: >-
        The request reached the content delivery network in front of the API
        but no response came back in time. **The body is an HTML page, not the
        JSON envelope every other status uses** — check `Content-Type` before
        parsing a 5xx. A `Retry-After` header is present, so an ordinary retry
        helper still works. Any route can answer this; the long-running
        routes are the likeliest.
      content:
        text/html:
          schema:
            type: string
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying.

x-tagGroups:
  - name: Get started
    tags:
      - overview
      - quickstart
      - auth-guide
      - models
      - pricing
  - name: Core concepts
    tags:
      - avatars-imx
      - audio-streaming
      - agent-lifecycle
  - name: SDKs
    tags:
      - sdk-python
      - sdk-swift
      - cli
  - name: Guides
    tags:
      - local-mode
      - building-avatars
  - name: Deployment
    tags:
      - deploy-livekit
      - deploy-self-hosted
      - deploy-embed
  - name: API reference
    tags:
      - Authentication
      - Agent Generation
      - Agent Management
      - Agent Context
      - Files
      - Dynamics
      - Embedding
      - Video
      - Billing
      - Account
      - Self-hosting
      - API secrets
      - Providers
      - Runtime sessions
      - Knowledge
      - Realtime
      - Status
      - Webhooks
  - name: Resources
    tags:
      - community
      - changelog
      - support

tags:
  ##########################################################################
  # Prose-only sections (no operations). Scalar lists them in the sidebar via
  # x-tagGroups. Each is one sentence and the page that owns the subject —
  # the docs site is the one writer for guides and concepts, and this spec
  # used to carry a second, older copy of every one of them.
  ##########################################################################
  - name: overview
    x-displayName: Overview
    description: "What bitHuman is, where an avatar renders and what you can build: [https://docs.bithuman.ai/api](https://docs.bithuman.ai/api)."
  - name: quickstart
    x-displayName: Quickstart
    description: "Your first talking avatar, with curl and a hosted agent: [https://docs.bithuman.ai/platforms/rest](https://docs.bithuman.ai/platforms/rest)."
  - name: auth-guide
    x-displayName: Authentication
    description: "API secrets, runtime tokens and where each surface reads its key: [https://docs.bithuman.ai/api/authentication](https://docs.bithuman.ai/api/authentication)."
  - name: models
    x-displayName: Models
    description: "Essence 2, Expression 2 and the first generation — what each does and where each runs: [https://docs.bithuman.ai/models](https://docs.bithuman.ai/models)."
  - name: pricing
    x-displayName: Pricing
    description: "Credits per minute by model and surface, creation costs and plans: [https://docs.bithuman.ai/pricing](https://docs.bithuman.ai/pricing)."
  - name: avatars-imx
    x-displayName: Avatars & .imx files
    description: "The downloadable model file, how it is addressed by agent code and how to inspect it: [https://docs.bithuman.ai/models/avatar-file](https://docs.bithuman.ai/models/avatar-file)."
  - name: audio-streaming
    x-displayName: Audio streaming
    description: "The push/drain contract every SDK shares — PCM in, lip-synced frames out: [https://docs.bithuman.ai/models/how-it-works#audio-in-frames-out](https://docs.bithuman.ai/models/how-it-works#audio-in-frames-out)."
  - name: agent-lifecycle
    x-displayName: Agent lifecycle
    description: "Generate, poll, resolve and drive an agent over the API and SDKs: [https://docs.bithuman.ai/api/agents](https://docs.bithuman.ai/api/agents)."
  - name: sdk-python
    x-displayName: Python SDK
    description: "`pip install bithuman` — open an avatar, render audio through it, take frames: [https://docs.bithuman.ai/platforms/python](https://docs.bithuman.ai/platforms/python)."
  - name: sdk-swift
    x-displayName: Swift SDK
    description: "The SwiftPM package for iPhone, iPad and Mac: [https://docs.bithuman.ai/platforms/ios](https://docs.bithuman.ai/platforms/ios)."
  - name: cli
    x-displayName: CLI (bithuman)
    description: "One binary for macOS Apple Silicon and Linux x86_64 — a live avatar in the browser or an MP4 render: [https://docs.bithuman.ai/platforms/cli](https://docs.bithuman.ai/platforms/cli)."
  - name: local-mode
    x-displayName: Local conversation brain
    description: "Run the conversation brain on your own Mac or Linux machine instead of the cloud: [https://docs.bithuman.ai/platforms/cli/local-brain](https://docs.bithuman.ai/platforms/cli/local-brain)."
  - name: building-avatars
    x-displayName: Building avatars
    description: "Get a pre-built avatar or generate one from a portrait: [https://docs.bithuman.ai/build/create-avatar](https://docs.bithuman.ai/build/create-avatar)."
  - name: deploy-livekit
    x-displayName: Managed (LiveKit plugin)
    description: "Drop an avatar into a LiveKit agent worker with the plugin: [https://docs.bithuman.ai/platforms/livekit](https://docs.bithuman.ai/platforms/livekit)."
  - name: deploy-self-hosted
    x-displayName: Self-hosting
    description: "Run Essence 2, Expression 2 and Essence 1 on your own hardware (a Mac, a Linux box, a phone or a browser tab); Expression 1 runs in the cloud only: [https://docs.bithuman.ai/deploy/self-hosted](https://docs.bithuman.ai/deploy/self-hosted)."
  - name: deploy-embed
    x-displayName: Browser embed
    description: "A talking avatar on any website with one iframe and an embed token: [https://docs.bithuman.ai/api/embedding](https://docs.bithuman.ai/api/embedding)."
  - name: community
    x-displayName: Community
    description: "Where to find bitHuman online, get help and contribute: [https://docs.bithuman.ai/support](https://docs.bithuman.ai/support)."
  - name: changelog
    x-displayName: Changelog & releases
    description: "Release notes and version history: [https://docs.bithuman.ai/changelog](https://docs.bithuman.ai/changelog)."
  - name: support
    x-displayName: Support & status
    description: "Help, Discord and the status page: [https://docs.bithuman.ai/support](https://docs.bithuman.ai/support)."
  - name: Authentication
    description: Verify API credentials with `POST /v1/validate`.
  - name: Agent Generation
    description: Create new avatar agents from prompts, portraits, and voice samples.
  - name: Agent Management
    description: Retrieve and update existing agents.
  - name: Agent Context
    description: Control live avatar sessions — make them speak or inject knowledge.
  - name: Files
    description: Upload images, videos, audio, and documents.
  - name: Billing
    description: Check credit balance, plan details, and usage history.
  - name: Account
    description: Identity, plan, credits, and pricing.
  - name: Self-hosting
    description: |
      The two routes a self-hosted or on-device runtime calls: check that its
      key is still live, and report the session time it served. See
      [Pricing](https://docs.bithuman.ai/pricing).
  - name: API secrets
    description: "Create, list and delete your account's API secrets: [https://docs.bithuman.ai/api/api-keys](https://docs.bithuman.ai/api/api-keys)."
  - name: Providers
    description: "Bring your own LLM, STT and TTS keys: [https://docs.bithuman.ai/api/providers](https://docs.bithuman.ai/api/providers)."
  - name: Runtime sessions
    description: "List live sessions, read a transcript, end or terminate a session, revoke every runtime key: [https://docs.bithuman.ai/api/runtime-sessions](https://docs.bithuman.ai/api/runtime-sessions)."
  - name: Knowledge
    description: "Ingest sources and build knowledge bases, with an organization API secret: [https://docs.bithuman.ai/api/knowledge](https://docs.bithuman.ai/api/knowledge)."
  - name: Realtime
    description: "OpenAI Realtime voice through bitHuman; the WebSocket relay is on [https://docs.bithuman.ai/api/realtime](https://docs.bithuman.ai/api/realtime)."
  - name: Status
    description: |
      Is the API currently refusing work? Unauthenticated, always HTTP 200 —
      read the `status` field. For a fleet-wide view use
      [status.bithuman.ai](https://status.bithuman.ai).
  - name: Webhooks
    description: |
      Register HTTPS endpoints to receive signed event notifications when
      async work finishes (`agent.ready`, `agent.failed`, `video.completed`, `video.failed`). Each
      delivery carries an `X-BitHuman-Signature: sha256=<hmac>` header — verify
      it with the secret returned once at creation.
  - name: Embedding
    description: Mint short-lived tokens for website avatar embeds.
  - name: Video
    description: |
      Render a finished talking-video mp4 from text or audio. Submit
      [`POST /v1/video/generate`](#operation/generateTalkingVideo) — async by
      default, or `wait: true` for a blocking render — and poll
      [`GET /v1/video/{job_id}`](#operation/getTalkingVideo).
  - name: Dynamics
    description: |
      Generate and manage conversational gesture animations (wave,
      nod, laugh, idle motions) for an avatar. Generate them
      asynchronously, then toggle on/off to make them the active model
      for live sessions. Gestures trigger based on keyword mapping
      during conversation.
