{
  "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\nthe cloud, on your own hardware, or in the browser. This is the REST\ncontract; the guides, the SDKs and the CLI are on\n[docs.bithuman.ai](https://docs.bithuman.ai).\n\nAll endpoints are relative to `https://api.bithuman.ai` and\nrequire an `api-secret` header.\n[Get an API secret →](https://www.bithuman.ai/developer/api-keys)\n\nThis machine-readable spec covers the developer surface. Two groups\nare documented only in the markdown guides, not here:\n[organizations](https://docs.bithuman.ai/api/organizations)\n(`/v2/organizations*`, `/v2/agents/{agent_code}/share-org`) and the\n[Realtime WebSocket relay](https://docs.bithuman.ai/api/realtime)\n(`wss://api.bithuman.ai/v1/realtime`). Purely internal / infrastructure routes —\nwebhook receivers, `/func_*` generation internals, health/telemetry,\nand realtime session plumbing — are excluded entirely: they are not\npart of the public developer contract and may change without notice.\n"
  },
  "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# → {\"valid\": true}\n"
          }
        ],
        "summary": "Validate API secret",
        "description": "Verify that your API secret is valid and your account is active.\n\nAlways returns HTTP 200. Inspect the `valid` field in the body\nto determine outcome — invalid or missing credentials yield\n`{\"valid\": false}` rather than a 401. The secret goes in the\n`api-secret` header; a secret sent as `Authorization: Bearer` is\nnot read and also yields `{\"valid\": false}`.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"expression-2\",\n    \"prompt\": \"You are a friendly fitness coach who motivates users.\",\n    \"image\": \"https://example.com/face.jpg\",\n    \"aspect_ratio\": \"9:16\"\n  }'\n# Always send model: essence-2 | expression-2 | auto (picks by subject).\n# → {\"success\": true, \"agent_id\": \"A95SXN5716\", \"status\": \"processing\"}\n# then poll GET /v1/agent/status/A95SXN5716\n"
          }
        ],
        "summary": "Generate a new agent",
        "description": "Create a new avatar agent from a text prompt, with optional image\nand audio assets. Poll `/v1/agent/status/{agent_id}` to check\nprogress.\n\n**Agent creation is image-only.** Provide a portrait `image` (or let\nthe prompt generate one) — bitHuman generates a **10-second\nidentity video internally**, authored to loop without a seam (its\nfirst and last frames match). A `video` field is rejected with\n`400 VIDEO_INPUT_NOT_SUPPORTED` before anything is billed.\n\n**Selecting a model.** Always send `model`: `essence-2`,\n`expression-2`, `auto`, `essence-1` or `expression-1`. An omitted\n`model` creates an `expression-1` agent. A model outside your plan\nreturns `403 PLAN_REQUIRED`.\n\n**Creation time depends on the model** (`expression-2` trains a\nper-identity model, so don't apply a 5-minute client timeout to it):\n\n| `model` | identity input | creation time |\n|---|---|---|\n| `essence-2` | image (or generated from prompt); a 10-second identity video is generated internally | a few minutes |\n| `expression-2` | image (or generated from prompt) | takes longer to build |\n| `auto` | image or prompt (classified automatically) | as the routed model (`essence-2` or `expression-2`) |\n| `essence-1` | image (or generated from prompt) | under 15 min |\n| `expression-1` (the default when `model` is omitted) | image (or generated from prompt) | about 1 min |\n\nTimes run from this call to `status: \"ready\"` on\n`GET /v1/agent/status/{agent_id}`. They are a typical range, not a\nguarantee, and individual runs can take longer: size timeouts in\nhours for the second-generation models.\n\n**Picking the right family** ([Choosing a model](https://docs.bithuman.ai/models#choosing-a-model)):\n`essence-2` for a real person; `expression-2` for a cartoon,\nstylized, animal, creature or robot character. Send\n`model: \"auto\"` to apply this rule automatically — the platform\nclassifies your input and records the recommended model either way.\n\n**Subject requirement:** every model except `expression-2` needs a\nclear, **real human face**: `essence-2`, `essence-1` and\n`expression-1` (also when `model` is omitted). The input is\nclassified before anything is billed; a cartoon, stylized\ncharacter, animal, creature or robot, or a photo with no face\nfound, is rejected with `422 MODEL_SUBJECT_MISMATCH` (nothing\ncharged) — use `expression-2` for those characters, or send\n`model: \"auto\"` to route automatically.\n\nFailures during generation surface as `status: \"failed\"` with an\n`error_message` on the status endpoint, and the creation credits\nare automatically refunded.\n",
        "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. A `file_id` or signed link from `POST /v1/files/upload` is accepted for files your own account uploaded.",
                    "example": "https://example.com/face.jpg"
                  },
                  "audio": {
                    "type": "string",
                    "description": "URL to audio for voice cloning, or a `file_id` or signed link from `POST /v1/files/upload`. Requires `attestation`.",
                    "example": "https://example.com/voice.wav"
                  },
                  "attestation": {
                    "type": "object",
                    "description": "Required when `audio` is sent. Your confirmation of the statement \"This is my own voice, or I have the speaker's permission to clone it.\" Without it, a request with `audio` returns `400 VOICE_CLONE_ATTESTATION_REQUIRED` before anything is uploaded or charged. Cloning someone else's voice without permission breaks the [acceptable use policy](https://www.bithuman.ai/legal/acceptable-use).",
                    "required": [
                      "attested",
                      "variant"
                    ],
                    "properties": {
                      "attested": {
                        "type": "boolean",
                        "const": true,
                        "description": "Must be `true`."
                      },
                      "variant": {
                        "type": "string",
                        "enum": [
                          "a"
                        ],
                        "description": "The wording version you confirmed. Send `\"a\"`."
                      }
                    },
                    "example": {
                      "attested": true,
                      "variant": "a"
                    }
                  },
                  "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.\n\n**`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 — Expression 2 takes longer to build; Essence 2 takes minutes; 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"
                    }
                  }
                }
              }
            }
          },
          "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`), or `audio` without a valid `attestation` (`VOICE_CLONE_ATTESTATION_REQUIRED`). 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
                    }
                  },
                  "voice_clone_attestation_required": {
                    "summary": "Voice sample (`audio`) without a consent attestation",
                    "value": {
                      "error": {
                        "code": "VOICE_CLONE_ATTESTATION_REQUIRED",
                        "message": "A voice clone needs your confirmation that this is your own voice, or that you have the speaker's permission to clone it. Send \"attestation\": {\"attested\": true, \"variant\": \"a\"}.",
                        "httpStatus": 400
                      },
                      "status": "error",
                      "status_code": 400
                    }
                  }
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# poll every 5 s until data.status is \"ready\" (or \"failed\").\n# \"success\" is a STEP marker written mid-run — not terminal unless progress == 1.0\n"
          }
        ],
        "summary": "Get agent generation status",
        "description": "Poll this endpoint to check generation progress. Status values:\n`processing`, `generating`, `completed`, `success` (all intermediate);\n`ready` (terminal success); `failed` (terminal error).\n\n**Keep polling past `generating`, `completed` and `success`** — those\nare intermediate states, not terminal. `completed` can appear early\n(even around ~5% `progress`), and `success` is a STEP-level marker the\nvoice/image and video steps each write (~20% and ~45% `progress`), so\ndo not stop on either. Only `ready` and `failed` mean polling should\nstop — or `success` when `progress` has also reached `1.0`.\n\n`current_step` reports the pipeline stage: `payment` (~2%) →\n`persona` (5–15%) → `voice_image` (~20%) → `video` (~45%, the\ninternally generated 10-second identity video — models that need\none) → `lip_sync` (70–99%, the\nmodel-specific identity step — training/distillation for\n`expression-2` / `essence-2`, so the longest stage) → `done`\n(100%). A rare `essence-1` path reports `awaiting_face_marking`\n(~35%).\n\nRecommended polling interval: 5 seconds. Size your overall timeout to\nthe model: minutes for `essence-2` and the first-generation engines,\nlonger for `expression-2` (it takes longer to build before\n`ready`). Poll until `status` is `ready` or `failed` (a boolean `final`\nfield, `true` for exactly those two, is coming). On the way `status` takes other values, such as\n`generating` and `success`, which are not final.\n\n**`ready` does not yet mean downloadable.** The agent can be launched\nand embedded as soon as it reports `ready`, but the downloadable\nmodel file is published separately, shortly afterwards. Until it is,\n`GET /v1/agent/{code}/model/download` answers\n`404 MODEL_ARTIFACT_NOT_READY` — a retryable code, not a failure. It\ncan take a few minutes after `ready`; poll the download while it\nanswers `404 MODEL_ARTIFACT_NOT_READY`.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"system_prompt\": \"You are a professional sales assistant.\"}'\n"
          }
        ],
        "summary": "Update an agent's prompt or providers",
        "description": "Update an existing agent's system prompt, its provider selection, or\nboth. The agent must already exist (use `POST /v1/agent/generate` to\ncreate one). Send at least one of `system_prompt` and `providers` — a\nbody with neither is rejected `400 MISSING_PARAM`.\n\n**An agent's `name` cannot be set here, and cannot be set at\ncreation either.** Names are generated from the agent's prompt and\nportrait. A body whose only field is `name` changes nothing and\nreturns `400 MISSING_PARAM`; sending `name` alongside `system_prompt`\nupdates the prompt and ignores the name.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# → {\"success\": true, \"agent_code\": \"A95SXN5716\", \"deleted\": true}\n"
          }
        ],
        "summary": "Delete an agent",
        "description": "Permanently delete an agent you own. Stored assets are cleaned up\nbest-effort; usage history is retained. Deleting a missing or\nnon-owned agent returns 404.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"message\": \"Hello! Welcome to our demo.\"}'\n"
          }
        ],
        "summary": "Make agent speak",
        "description": "Send text for the avatar to speak aloud. With `room_id` it goes to that\none session; without it, to every deliverable session of the agent.\nThe agent must be in at least one active LiveKit room.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "List an agent's live sessions",
        "description": "The sessions this agent has open now. Pass a returned `room_id` to\n/speak or /add-context to address ONE session instead of broadcasting.\n`deliverable` is false when the room is live but no agent worker is\nattached — /speak on such a room returns 404.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"context\": \"The customer just purchased a premium plan.\"}'\n"
          }
        ],
        "summary": "Add context to agent",
        "description": "Inject background knowledge into the agent's context. The avatar\nwon't say this aloud, but will use the information in future responses.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"model\": \"expression-2\"}'\n# → {\"success\": true, \"status\": \"processing\", ...} — poll\n#   GET /v1/agent/status/A95SXN5716 until supported_models lists it\n"
          }
        ],
        "summary": "Add a model to an existing agent",
        "description": "Add an avatar model to an agent you already created — no need to\nre-create the agent or regenerate its persona/voice/image. The agent\nmust be in the `ready` state (else `409 AGENT_NOT_READY`).\n\n| model | what happens | prerequisites | credits | time |\n|---|---|---|---|---|\n| `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) |\n| `expression-2` | trains the per-identity Expression 2 model from the stored image | stored image | 2000 | the same training an `expression-2` creation runs (see [creation times](#operation/generateAgent)) |\n| `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 | about 2–2.5 h: adding Essence 2 to an existing agent still trains it (a new `essence-2` agent is ready in minutes) |\n| `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 |\n\nAsynchronous adds return `status: \"processing\"` — poll\n[`GET /v1/agent/status/{agent_id}`](#tag/Agent-Generation/operation/getAgentStatus)\nuntil `supported_models` contains the new family. Charges are refunded\nautomatically if the add fails; re-POSTing the same model never\ndouble-charges, and returns `status: \"ready\"` with `credits: 0` once\nthe model already exists.\n",
        "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\ncurl -L -o \"A17ZTB0222.imx\" \\\n  \"https://api.bithuman.ai/v1/agent/A17ZTB0222/model/download?model=expression-2\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# → A17ZTB0222.imx  (add ?redirect=false for the URL as JSON)\n"
          }
        ],
        "summary": "Download an agent's model artifact",
        "description": "Download the generated model file for an agent you own. The family\ndefaults to the agent's own model; override with `?model=<family>`\n(canonical names, legacy names, and runtime tier slugs all fold onto\ntheir family). Artifact per family:\n\n| family | artifact |\n|---|---|\n| `essence-1` | `<code>.imx` (the portable IMX container) |\n| `essence-2` | `<CODE>.imx` — the standard Essence 2 artifact (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). |\n| `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 |\n| `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` |\n\nThe default response is a **302 redirect** to the artifact — the\npublic URL for `essence-1`, a **1-hour signed URL** on a private\nbucket for the other families — so `curl -L` and browser anchors just\nwork. Pass `?redirect=false` to get the URL as JSON instead.\n\n**Name the file yourself** (`curl -L -o \"<CODE>.imx\"`). The redirect\ntarget does not always send a `Content-Disposition` header — the\npublic `essence-1` URL sends none — so a filename-from-the-header\nflag such as `curl -LOJ` silently falls back to the last path segment\nof the request URL and writes a file called `download`. With\n`?redirect=false` the JSON carries the intended name in `filename`.\n\nThe launchability gate doubles as the download gate: a family the\nagent can't be launched as (missing from `supported_models`) returns\n`409 MODEL_NOT_GENERATED`. A family the agent supports whose artifact\nhasn't been published to the download store yet returns\n`404 MODEL_ARTIFACT_NOT_READY` — the message says when to retry;\n**poll on that code**.\n\n**Every grant is recorded.** A model download costs **0 credits** on\nthe [pricing table](https://docs.bithuman.ai/pricing) — only\nruntime minutes bill — but the grant still writes exactly one usage row\nbefore the URL is issued, under the family's self-hosted pricing code,\nand hands you its id in `X-Bithuman-Meter-Id`. The rows show up in\n`GET /v1/usage` at 0 credits; they are a record of what you fetched,\nnot a charge. The order matters both ways: a meter that refuses\n(`402`) or fails (`500`) returns no URL at all.\n\nThe [bitHuman CLI](https://docs.bithuman.ai/platforms/cli/reference) wraps\nthis endpoint: `bithuman pull <AGENT_CODE>` downloads the artifact,\nrecognizes its model family, and prints what to do next.\n\n**Showcase identities need no credential.** The `essence-2` and\n`expression-2` identities listed by\n[`GET /v1/models/showcase`](#tag/Agent-Management/operation/getShowcaseManifest)\nare served to anonymous callers — same 302, same 1-hour signed URL, and\nthe owner is charged **0 credits**. Every other agent still needs the\n`api-secret` header (or `Authorization: Bearer <api-secret | runtime\ntoken>`) and must be owned by the caller; an anonymous request for one\nanswers `401 MISSING_AUTH`, the same answer an unknown code gets.\n",
        "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": {
          "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
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "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."
              }
            }
          },
          "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"file_url\": \"https://example.com/avatar.jpg\", \"file_type\": \"image\"}'\n"
          }
        ],
        "summary": "Upload a file",
        "description": "Upload images, videos, audio, or documents — by public URL or as\nbase64. Files are organized by type, and the type is read from the\nfile's own bytes: `file_type` and the filename extension are both\ncross-checked against them, never trusted over them. Files are stored\nprivately. The response returns a signed `file_url` (valid 7 days) and\na `file_id` to pass to other endpoints instead of the URL.\n",
        "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_id": {
                          "type": "string",
                          "description": "Private reference to the uploaded file. Pass it instead of a URL to the endpoints that take an uploaded file; it works only for files your own account uploaded.",
                          "example": "3f2b9c14-8a7e-4d21-9f10-2c5b6e0a11dd"
                        },
                        "file_url": {
                          "type": "string",
                          "description": "Signed link to the private file, valid 7 days."
                        },
                        "url_expires_in": {
                          "type": "integer",
                          "description": "Lifetime of `file_url` in seconds.",
                          "example": 604800
                        },
                        "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:\n\n- `DOWNLOAD_FAILED` — `file_url` could not be downloaded:\n  unreachable, not publicly readable, or not an ordinary public\n  `http(s)` address.\n- `VALIDATION_ERROR` — `file_data` was not valid base64, both\n  `file_url` and `file_data` were sent, or `file_type` was a value\n  this API does not define. The accepted values are `auto`,\n  `image`, `video`, `audio`, `document` and `pdf`; anything else is\n  refused rather than quietly treated as `auto`, so a typo cannot\n  silently change where your file lands.\n- `MISSING_PARAM` — neither `file_url` nor `file_data` was sent.\n",
            "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:\n**10 MB** images, **100 MB** video, **25 MB** audio and documents.\nThe category is read from the file's own bytes, so the limit that\napplies is the one for what the file actually is.\n",
            "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\nraise it:\n\n- **The bytes are not a supported type.** Supported: JPEG, PNG,\n  GIF, WebP, BMP · MP4, MOV, WebM, AVI, WMV, FLV, Matroska ·\n  MP3, WAV, FLAC, AAC, OGG, M4A · PDF, Word, Excel, PowerPoint,\n  RTF and plain text.\n- **The bytes contradict `file_type`.** Sending\n  `file_type: \"image\"` with a text file is refused here rather than\n  stored under the wrong category.\n- **The filename extension is not one of the supported ones.** For\n  a base64 upload that is `file_name`; for a URL upload it is the\n  last path segment of `file_url`. A name with no extension, or\n  one like `.exe` or `.sh`, is refused.\n- **The file would run in a browser.** SVG, HTML and any text\n  carrying a script are refused outright: an uploaded file is\n  served from a public address, and a file that executes there\n  would run as if it came from us.\n",
            "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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.\ncurl -s https://api.bithuman.ai/v1/models/showcase | jq '.models[] | {slug, model, size}'\n"
          },
          {
            "lang": "shell",
            "label": "bitHuman CLI",
            "source": "bithuman list                              # this catalogue, with what is cached\nbithuman pull bolt                         # download one ahead of time\nbithuman render bolt hello.wav -o out.mp4  # or let render download it\n"
          }
        ],
        "summary": "List the freely downloadable showcase models",
        "description": "The catalogue of **Essence 2** and **Expression 2** showcase identities\nwhose model weights anyone may download — no api-secret, no account.\n\nEach entry's `url` is\n[`GET /v1/agent/{code}/model/download`](#tag/Agent-Management/operation/downloadAgentModel),\nwhich answers a **302** to a 1-hour signed URL for anonymous callers\ntoo. The grant is still recorded (one usage row) and costs the agent's\nowner **0 credits** — an anonymous download is free or refused, never\nbilled to the owner.\n\nThe list is generated from the download route's own admission\npredicate, so a code listed here is a code the route serves: an\n`essence-2` or `expression-2` identity that is ready, listed in the\nagent gallery, owns its own artifact, and whose owner has authorised\nanonymous distribution of the weights. Agents that are merely public in\nthe gallery are **not** here — being viewable is a different permission\nfrom the weights being distributable.\n\nThe shape is the bitHuman CLI's manifest v2; `bithuman list` and\n`bithuman pull` read this catalogue.\n\n**Performance.** The reply carries no frame rates.\n`note_performance` maps each family in the catalogue to the\n[performance page](https://docs.bithuman.ai/performance), where the frame rates are\nmeasured and published for every device.\n\n`sha256` is deliberately empty: no content digest is recorded for these\nobjects yet, and the CLI skips verification on an empty value rather\nthan failing. Verify `size` and the file itself (`bithuman open`)\ninstead.\n\nThrottled per IP; the reply is cached for 60 seconds.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Check credit balance",
        "description": "Returns the current credit balance, plan details, and estimated minutes\nremaining for each usage type. Always scoped to the **authenticated\naccount** (the owner of the `api-secret`) — there is no way to look up\nanother user.\n",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "apiSecret": []
          }
        ],
        "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
                        },
                        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"agent_id\": \"A23WJF0199\", \"fingerprint\": \"<device-fingerprint>\"}'\n# → data.token is a 1-hour JWT for the embed widget\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"agent_code\": \"A80HVD8577\"}'\n# → {\"scoped_token\": \"...\", \"user_id\": \"...\", \"agent_code\": \"A80HVD8577\", \"expires_at\": 1757280000}\n"
          }
        ],
        "summary": "Mint a scoped runtime token for one agent",
        "description": "Exchange your API secret, **server-side**, for a 1-hour token bound to a\nsingle `agent_code` — so a browser, a phone app or any other client you\ndo not fully control never holds your master secret.\n\nThis is the credential the on-device paths accept. It works as\n`Authorization: Bearer <scoped_token>` on\n[the model download route](#operation/downloadAgentModel), including\nthe `?member=` fetch the browser and Android runtimes use.\n\nThe scope is real: presenting the token for a **different** agent code\nanswers `404`, exactly like a code you do not own. It expires in one\nhour and is not refreshable — mint another.\n\n**LiveKit plugin.** With `\"scope\": \"livekit-cloud\"` this mints the token\nto pass to `livekit-plugins-bithuman` as `api_secret` instead of your API\nsecret. The plugin writes that value into LiveKit room attributes, where\nevery participant can read it. This token can only start this agent's\ncloud avatar session. It is also bound to `room_name` and `livekit_url`\nwhen you send them, and it bills your account. For another agent, room\nor LiveKit server it answers `403`. It cannot download the model or call\nany other endpoint. See [LiveKit integration](/platforms/livekit).\n\nCall this from your server. Never ship the api-secret this endpoint\nconsumes to a client.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"agent_code\": \"A80HVD8577\"}'\n# → data.token is a short-lived runtime JWT the SDK uses to run a session\n"
          },
          {
            "lang": "shell",
            "label": "cURL (start a cloud avatar)",
            "source": "curl -X POST https://api.bithuman.ai/v1/runtime-tokens/request \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"mode\": \"gpu\", \"model\": \"essence-2\", \"agent_id\": \"<your agent code>\",\n       \"livekit_url\": \"wss://your-project.livekit.cloud\",\n       \"livekit_token\": \"<avatar join token>\", \"room_name\": \"your-room\"}'\n# → {\"avatar_session_started\": true, \"model\": \"essence-2\", ...}\n"
          }
        ],
        "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`.\n\n**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).\n\n**`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-19, 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-19, 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-19. 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-19."
                        },
                        "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-19, 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-19",
                    "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"agent_id\": \"A95SXN5716\"}'\n# → {\"success\": true, \"status\": \"processing\"} — poll GET /v1/dynamics/A95SXN5716\n"
          }
        ],
        "summary": "Generate gesture animations",
        "description": "Create gesture animations (wave, nod, smile, laugh, etc.) for an agent.\nThese can be triggered during live sessions via keyword mapping.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"dynamics\": {\"enabled\": true}, \"toggle_enabled\": true}'\n"
          }
        ],
        "summary": "Update dynamics configuration",
        "description": "Update gesture settings for an agent. After a successful update,\nmovements regeneration is automatically triggered in the background.\n",
        "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.\nFields: `enabled` (boolean), `batch_results` (gesture results),\n`result` (model path/hash when generated), `talking` (default model path/hash).\n",
                    "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.\n"
                  }
                }
              }
            }
          }
        },
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"model\": \"essence-2\",\n    \"agent_code\": \"A95SXN5716\",\n    \"input\": {\"type\": \"audio\", \"audio_url\": \"https://docs.bithuman.ai/samples/speech-16k-short.wav\"}\n  }'\n# → {\"success\": true, \"job_id\": \"vid_3f9a2c1b8e7d4a6f0b21\", \"status\": \"processing\"}\n# add \"wait\": true to block (≤ ~90 s) for the finished video_url\n"
          }
        ],
        "summary": "Generate a talking video",
        "description": "Render a talking-video (mp4) from **your audio**, driven by one of\nyour agents.\n\n**Two modes:**\n- **Async (default)** — returns immediately with a `job_id`; poll\n  [`GET /v1/video/{job_id}`](#operation/getTalkingVideo) for the finished\n  video. Use this for anything but the shortest clips — renders can take a\n  while, so you don't want to hold the connection.\n- **Sync** — set `wait: true` (or `?wait=true`). The call blocks until the\n  render finishes (up to ~90s) and returns the `video_url` directly in this\n  response. If the render is still running at that cap you get the async\n  response (`status: processing`) instead — just poll the `job_id`.\n\n**Input:** your own audio. `audio_url` (a hosted WAV/MP3: a recording, or\nthe output of any text-to-speech tool) drives the render directly.\n\nThe output is a public CDN URL. `essence-2` renders at\n**1080p** by default (1080×1920 portrait or 1920×1080 landscape, matching\nthe source orientation and capped at the source's long side);\n`expression-2` renders at its native 416×720. Limits: max **120 seconds**\nof output.\n",
        "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\n`expression-1` (4 credits/min) are the first-generation models,\nstill supported. `expression-2` (4 credits/min) and `essence-2`\n(4 credits/min) are the second-generation models.\n",
                    "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, or a `file_id` or signed link from `POST /v1/files/upload` for a file your own account uploaded.",
                        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# poll until status is \"completed\" — then video_url is the public mp4\n"
          }
        ],
        "summary": "Get talking-video status",
        "description": "Poll a talking-video render job. While rendering, `status` is `processing`.\nWhen done, `status` is `completed` and the response carries `video_url`,\n`duration_seconds`, and `credits_charged`. On failure, `status` is `failed`\nand the up-front charge is automatically refunded.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "List agents",
        "description": "List the agents owned by your API secret, newest first. Paginated.\n\nEach item is a **summary**, not the full agent record: it carries the\nidentity, the state and the launch surface, and nothing else. The\nprompt, the progress fields, the media URLs and the model handle are\nonly on [`GET /v1/agent/{code}`](#operation/getAgent) — a caller that\nreads them from a list item reads fields that are not there.\n\n**Deleted agents are not listed and not counted.** Every page holds\n`limit` items except the last. `pagination.total` is the number of\nagents you reach by paging, and it is the same on every page. Pass\n`?status=ready` if you want only agents that finished generating.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Usage history",
        "description": "Return your account's usage / activity history, newest first. Each row\nis a metered event. `credits_change` is the signed credit delta — usage\nevents are recorded as **positive** credits consumed.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\"url\": \"https://example.com/bithuman/webhook\", \"events\": [\"agent.ready\", \"agent.failed\"]}'\n# → data.secret is returned ONCE — store it to verify signatures\n"
          }
        ],
        "summary": "Create a webhook",
        "description": "Register an HTTPS endpoint to receive signed event notifications. The\nresponse includes a one-time `secret` used to verify the\n`X-BitHuman-Signature` header — store it; it is never returned again.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n# delivers a one-off \"ping\" event to your endpoint\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "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"
          }
        }
      }
    },
    "/v1/auth/validate": {
      "post": {
        "operationId": "validateSelfHostedKey",
        "x-codeSamples": [
          {
            "lang": "shell",
            "label": "cURL",
            "source": "curl -X POST https://api.bithuman.ai/v1/auth/validate \\\n  -H \"Authorization: Bearer $BITHUMAN_API_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"billing_type\": \"self-hosted-essence-2-model\"}'\n# → {\"user_id\": \"...\", \"key_id\": \"key_1a2b3c4d5e6f7a8b\"}\n"
          }
        ],
        "summary": "Live key check for a self-hosted runtime",
        "description": "The bitHuman SDKs, the CLI and the plugins call this for you when a\nself-hosted session starts and while it runs; you call it only if you\nbuild your own runtime. Deleting a key in the dashboard stops a running\nself-hosted session at its next check.\n\nA runtime treats the three answers differently:\n\n| status | meaning | what a runtime should do |\n|---|---|---|\n| `200` | valid | serve |\n| `401` | **definite**: unknown, revoked or disabled | stop |\n| `503` | we cannot tell right now | retry, keep serving |\n\nA `503` is never a statement about your key: keep serving and retry.\n",
        "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\nthe key for the product you run.\n",
                    "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\n`user_id` must be treated as a failure — an\nidentity-less \"ok\" cannot be billed to anybody.\n"
                    },
                    "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\nsent) and keep serving — this is not a statement about your key.\n",
            "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 \\\n  -H \"Authorization: Bearer $BITHUMAN_API_SECRET\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"session_id\":\"s-1\",\"install_id\":\"i-1\",\"seq\":1,\n       \"served_s\":60,\"billing_type\":\"self-hosted-essence-2-model\"}'\n"
          }
        ],
        "summary": "Report usage from a self-hosted session",
        "description": "The bitHuman SDKs, the CLI and the plugins call this for you: a\nself-hosted or on-device session reports its running time here, and\nthat is what bills it ([Pricing](https://docs.bithuman.ai/pricing)).\nYou call it only if you build your own runtime.\n\nA report covers the seconds the session ran, idle included, not frames\nrendered. The product (`billing_type`) is fixed by the first report of a\nsession.\n\nRead **only the status code**:\n\n| status | meaning | what a runtime should do |\n|---|---|---|\n| `2xx` | the interval is recorded | count it as reported |\n| `402` | balance depleted / account suspended | **keep rendering**, report it again later |\n| `401` | definite bad key | stop |\n| `503` | we cannot tell right now | retry |\n\nA `402` never stops a running session; the next session is refused\nuntil you top up. A retried report (same `session_id`, `install_id`\nand `seq`) is never charged twice.\n",
        "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\nserver-side; wall-clock, idle included.\n"
                  },
                  "billing_type": {
                    "type": "string",
                    "description": "The self-hosted product wire key. Always send the key\nfor the product you run.\n",
                    "example": "self-hosted-essence-2-model"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The interval is durably recorded. Advance the meter clock. Until 2026-10-19, 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-19. 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\n`topup_url`) or account suspended (`account_suspended`). Do **not**\nadvance the meter clock; keep rendering.\n",
            "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-19, a\nFree account's API secret, with an `upgrade_url`; or a product your\nplan does not include.\n",
            "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" -H \"content-type: application/json\" \\\n  -d '{\"alias\":\"prod-server\"}'\n"
          }
        ],
        "summary": "Create an API secret",
        "description": "Creates an API secret on your account. The response is the **only**\none that returns the full secret: store it now. Send `{}` for an\nauto-generated alias; a request with no body returns `400`.\n",
        "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\"\n"
          }
        ],
        "summary": "List API secrets",
        "description": "Your API secrets, masked. The raw secret is never returned here, only\n`key_display`. `name`, `key` and `alias` all carry the alias label.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Reveal an API secret (console only)",
        "description": "Console-only: only the signed-in owner can reveal a stored secret, in\nthe Console under Developer → API Secrets. Called with an API secret\nit always returns `403 SECRET_REVEAL_CONSOLE_ONLY`, so one leaked\nsecret cannot read your others.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Delete an API secret",
        "description": "Removes an API secret and revokes it at the runtime at once. Only that\nsecret stops working; to stop every secret, use\n`POST /v2/{user_id}/runtime/revoke-all`.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Get provider config",
        "description": "Your configured provider keys (BYOK). Sensitive keys are returned only\nas `{present, hint, updated_at}`, never in full.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" -H \"content-type: application/json\" \\\n  -d '{\"entries\":[{\"platform\":\"openai\",\"label\":\"OpenAI\",\"credentials\":{\"apiKey\":\"sk-…\"}}]}'\n"
          }
        ],
        "summary": "Update provider config",
        "description": "Replaces your provider list: `entries` is the **whole** list, and\n`{\"entries\": []}` clears every provider. Existing entries are matched by\n`id` and merged (unspecified credentials are kept); new entries get a\ngenerated id. Returns the same masked shape as `GET`.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "List sessions",
        "description": "Your sessions plus live account KPIs (`active_count`,\n`live_burn_rate_cr_per_min`, `credits_this_hour`, account-wide over the\nlast hour). A transient `500 UPSTREAM_ERROR` can be retried.\n",
        "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,
                        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Read a transcript",
        "description": "The conversation, oldest first. Only cloud voice and chat sessions\nrecord a transcript; self-hosted sessions return `messages: []`.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" -H \"content-type: application/json\" -d '{}'\n"
          }
        ],
        "summary": "Terminate a session",
        "description": "Stops **one** session; never touches an API secret. The session stops\ncounting toward your plan's concurrent sessions at once.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" -H \"content-type: application/json\" -d '{}'\n"
          }
        ],
        "summary": "Revoke all runtime keys",
        "description": "The emergency stop: disables **every** runtime key on the account. Live\nsessions stop within one refresh cycle (about 5 minutes). It does not\ndelete your API secrets; creating a new API secret clears the\nsuspension.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "Read a cloud avatar session",
        "description": "A [cloud avatar](https://docs.bithuman.ai/platforms/livekit/cloud-avatar)\nsession you started: `status` (`starting`, `live` or `ended`), times,\n`billed_seconds` and `credits` so far.\n",
        "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\" \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\"\n"
          }
        ],
        "summary": "End a cloud avatar session",
        "description": "Ends a [cloud avatar](https://docs.bithuman.ai/platforms/livekit/cloud-avatar)\nyou started, by the `session_id` its start returned. No body. Billing\nstops at this call and the concurrent-session slot frees at once.\nCalling it again returns the same answer and bills nothing.\n",
        "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 \\\n  -H \"api-secret: $ORG_API_SECRET\" -H \"content-type: application/json\" \\\n  -d '{\"url\":\"https://example.com/help\",\"name\":\"Help Center\",\"sync_freq\":\"weekly\"}'\n"
          }
        ],
        "summary": "Ingest a file or URL",
        "description": "Adds one source to your organization: an uploaded file\n(`multipart/form-data`, fields `file` up to 100 MB and `name`) or a URL\nto fetch (JSON). Returns the file row at status `uploaded`; conversion\nruns in the background. Needs an organization API secret; a personal\nsecret returns `403`.\n",
        "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 \\\n  -H \"api-secret: $ORG_API_SECRET\" -H \"content-type: application/json\" \\\n  -d '{\"name\":\"Support KB\",\"file_ids\":[\"3f2b9c14-8a7e-4d21-9f10-2c5b6e0a11dd\"]}'\n"
          }
        ],
        "summary": "Create a knowledge base",
        "description": "Creates a knowledge base and, by default, starts its first build.\n`build_dispatched` is `true` only when `build` was `true`, files were\nattached and the build queued.\n",
        "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\"\n"
          }
        ],
        "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 \\\n  -H \"api-secret: $ORG_API_SECRET\"\n"
          }
        ],
        "summary": "Rebuild a knowledge base",
        "description": "Re-indexes a knowledge base after its sources change. Returns the\nqueued build row; indexing runs in the background. Builds run one at a\ntime per account, up to 20 per day.\n",
        "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 \\\n  -H \"api-secret: $BITHUMAN_API_SECRET\" -H \"content-type: application/json\" \\\n  -d '{\"action\": \"hangup\", \"call\": \"<X-Bithuman-Call>\"}'\n"
          }
        ],
        "summary": "Start or end a WebRTC Realtime call",
        "description": "Starts a WebRTC voice call: send the SDP offer as the body\n(`Content-Type: application/sdp`) with `?model=gpt-realtime-mini`; a\n`201` carries the SDP answer, and the `X-Bithuman-Call` header names\nthe call. End it with a JSON body `{\"action\": \"hangup\", \"call\": …}` —\nbilling stops when you hang up. See\n[Realtime](https://docs.bithuman.ai/api/realtime#webrtc).\n",
        "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\n# → {\"status\":\"ok\",\"families\":{\"sessions\":{\"status\":\"ok\"}, ...}}\n"
          }
        ],
        "summary": "API readiness",
        "description": "Whether the API is refusing work right now. No credential required.\nIt always answers HTTP 200: read the `status` field (`ok`, `degraded`\nor `unhealthy`). For the overall service status, use\n[status.bithuman.ai](https://status.bithuman.ai).\n",
        "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\n[www.bithuman.ai/developer/api-keys](https://www.bithuman.ai/developer/api-keys).\n"
      },
      "bearerApiSecret": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same API secret, sent as `Authorization: Bearer <api-secret>`. Self-\nhosted runtimes send it this way; the classic `api-secret` header works\non the same routes.\n"
      }
    },
    "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\nmachine-readable word a runtime switches on; `error.code`,\n`error.httpStatus` and `error.message` are the platform envelope every\nother endpoint uses. Both are present on purpose — the self-hosted\ncontainer's key validator reads `type`, and the additive `code` means a\ndeveloper switching on `error.code` no longer gets `undefined` from\nthese routes.\n",
        "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"
          },
          "planes": {
            "type": "object",
            "description": "Readiness for each place the agent runs, present only on the `essence-2` entry of an Essence 2 agent on the current engine. Absent (never `null`) for every other agent and model. `cloud` is the agent's state in the bitHuman cloud; `device` is the state of the copy for your own devices (the Swift, Android and Python SDKs, the CLI and the browser's `render=local`).",
            "properties": {
              "cloud": {
                "type": "string",
                "enum": [
                  "pending",
                  "ready",
                  "failed"
                ],
                "description": "The agent's state in the bitHuman cloud; the same value as `state`.",
                "example": "ready"
              },
              "device": {
                "type": "string",
                "enum": [
                  "absent",
                  "preparing",
                  "ready",
                  "failed"
                ],
                "description": "`absent` not built yet; `preparing` requested or building; `ready` built and downloadable; `failed` the build stopped.",
                "example": "preparing"
              }
            }
          }
        }
      },
      "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. Stop on `ready` or `failed`; a `final` field is coming.",
            "example": "ready"
          },
          "final": {
            "type": "boolean",
            "description": "Coming: not sent yet. When it is, `true` only when `status` is `ready` or `failed` (stop polling) and `false` for every other value, including `success`. Until then, stop on `ready` or `failed`.",
            "example": true
          },
          "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.\nVerify deliveries with\n`sha256 = HMAC(secret, \"{X-BitHuman-Timestamp}.{raw_body}\")`\nand compare against the `X-BitHuman-Signature` header.\n",
                "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\npresent but invalid; `MISSING_AUTH` when it is absent. The HTTP status\nequals `status_code` (401).\n",
        "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\nthe account draws from one bucket — and **per cost tier**, so a burst of\nreads never uses up your generate budget. Your plan sets the number for\neach tier; the matrix, the tier each route belongs to and a retry\nrecipe are on\n[Rate limits](https://docs.bithuman.ai/api/rate-limits).\n\nWait `Retry-After` seconds and retry. The `X-RateLimit-*` headers let a\nclient slow down *before* it gets here, but header coverage is uneven\nacross routes — read them defensively and fall back to exponential\nbackoff when they are absent.\n\nOn a route that can also be called without an API secret, an anonymous\ncall is metered per network address instead: same body, same\n`Retry-After`, no `X-RateLimit-*`.\n",
        "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\nown credential and are checked by the service behind them, so there is\nno account to meter at the edge — they get one flat limit per\naddress (120 requests/minute) instead of the per-plan cost tiers, and\nno `X-RateLimit-*` headers. Wait `Retry-After` seconds and retry. See\n[Rate limits](https://docs.bithuman.ai/api/rate-limits).\n",
        "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\nminute. Only failures spend budget, so a runtime presenting a live key\nnever reaches this, however often it calls. It says nothing about\nwhether your key is valid: wait `Retry-After` seconds, retry, and read\nthe answer you get then. See\n[Rate limits](https://docs.bithuman.ai/api/rate-limits).\n",
        "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": [
    {
      "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[expression-2]\"` — 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 package",
      "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 and arm64 — 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\nkey is still live, and report the session time it served. See\n[Pricing](https://docs.bithuman.ai/pricing).\n"
    },
    {
      "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 —\nread the `status` field. For a fleet-wide view use\n[status.bithuman.ai](https://status.bithuman.ai).\n"
    },
    {
      "name": "Webhooks",
      "description": "Register HTTPS endpoints to receive signed event notifications when\nasync work finishes (`agent.ready`, `agent.failed`, `video.completed`, `video.failed`). Each\ndelivery carries an `X-BitHuman-Signature: sha256=<hmac>` header — verify\nit with the secret returned once at creation.\n"
    },
    {
      "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\n[`POST /v1/video/generate`](#operation/generateTalkingVideo) — async by\ndefault, or `wait: true` for a blocking render — and poll\n[`GET /v1/video/{job_id}`](#operation/getTalkingVideo).\n"
    },
    {
      "name": "Dynamics",
      "description": "Generate and manage conversational gesture animations (wave,\nnod, laugh, idle motions) for an avatar. Generate them\nasynchronously, then toggle on/off to make them the active model\nfor live sessions. Gestures trigger based on keyword mapping\nduring conversation.\n"
    }
  ]
}
