Docs index: /llms.txt · every page as Markdown: add .md

‹ API reference

Providers (BYOK)

Bring your own LLM, STT and TTS keys, stored encrypted, for your agents.

Overview

The Providers API lets you bring your own provider keys (BYOK) — e.g. your own OpenAI or ElevenLabs key — for your agents to use. Keys are stored encrypted at rest and are never returned in plaintext; reads return only presence, a masked hint, and timestamps.

Base URL https://api.bithuman.ai. Authenticate with the api-secret header. The {user_id} in the path is your account id — get it from GET /v1/me.

Set it once in your shell before the examples below — with $USER_ID unset the paths collapse to /v2//… and the API answers 404 NOT_FOUND:

export USER_ID=$(curl -s https://api.bithuman.ai/v1/me \
  -H "api-secret: $BITHUMAN_API_SECRET" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['data']['user_id'])")

Get provider config

GET /v2/{user_id}/providers getProviders

GET /v2/{user_id}/providers — your configured providers, secrets masked.

Sensitive keys (apiKey, api_key, secret, apiSecret, clientSecret, accessToken, privateKey) are returned only as {present, hint, updated_at} — never in full.

curl

curl "https://api.bithuman.ai/v2/$USER_ID/providers" \
  -H "api-secret: $BITHUMAN_API_SECRET"

Python

import os, requests

resp = requests.get(
    f"https://api.bithuman.ai/v2/{os.environ['USER_ID']}/providers",
    headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())

Node

const resp = await fetch(`https://api.bithuman.ai/v2/${process.env.USER_ID}/providers`, {
  headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
  "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" }
        }
      }
    ]
  }
}

Update provider config

PUT /v2/{user_id}/providers updateProviders

PUT /v2/{user_id}/providers — replace your provider list. Send the full set of entries you want stored.

Body: { "entries": ProviderEntry[] }. Each entry:

FieldTypeRequiredDescription
platformstringyesProvider id, e.g. openai, elevenlabs, cartesia, grok.
idstringnoExisting entry id to update; omit to add a new one.
labelstringnoDisplay name; defaults to the platform name.
credentialsobjectnoKey/value credentials. Sensitive keys are encrypted at rest.
optionsobjectnoPer-capability configuration.
clearSecretsstring[]noSensitive credential keys to remove.
metadataobjectnoFreeform metadata.

Returns the same masked shape as GET. Existing entries are matched by id and merged (unspecified credentials are preserved); new entries get a generated id.

entries replaces the whole list. Sending {"entries": []} clears all providers — to edit one provider, send the full set you want to keep. To drop a single secret, use clearSecrets.

Errors: 401/403 auth · 404 account not found · 500 on a storage failure.

curl

curl -X PUT "https://api.bithuman.ai/v2/$USER_ID/providers" \
  -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
  -d '{"entries":[{"platform":"openai","label":"OpenAI","credentials":{"apiKey":"sk-…"}}]}'

Python

import os, requests

resp = requests.put(
    f"https://api.bithuman.ai/v2/{os.environ['USER_ID']}/providers",
    headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
    json={
        "entries": [
            {
                "platform": "openai",
                "label": "OpenAI",
                "credentials": {"apiKey": "sk-…"},
            },
        ],
    },
)
print(resp.json())

Node

const resp = await fetch(`https://api.bithuman.ai/v2/${process.env.USER_ID}/providers`, {
  method: "PUT",
  headers: {
    "api-secret": process.env.BITHUMAN_API_SECRET,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    entries: [
      { platform: "openai", label: "OpenAI", credentials: { apiKey: "sk-…" } },
    ],
  }),
});
console.log(await resp.json());

Point an agent at your provider

Registering a key does not change any agent by itself — it just stores the credential. Each agent chooses its providers independently, so the last step is to attach the entry you registered to the agent that should use it.

POST /v1/agent/{agent_code} — set the per-capability selection. Send only the capabilities you want to change; the rest of the agent’s settings are preserved. The capabilities: llm is the language model, stt speech-to-text, tts text-to-speech, and realtime one speech-to-speech model that does all three.

CapabilityValueMeaning
llm / stt / tts / realtime"default"Use the bitHuman-managed provider (the default).
llm / stt / tts / realtime{"mode": "custom", "provider_id": "<id>"}Use one of your registered entries.

provider_id is the id returned by GET/PUT /v2/{user_id}/providers.

curl

curl -X POST https://api.bithuman.ai/v1/agent/A12345678 \
  -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
  -d '{"providers": {"llm": {"mode": "custom", "provider_id": "b7e1…"}}}'

Python

import os, requests

resp = requests.post(
    "https://api.bithuman.ai/v1/agent/A12345678",
    headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
    json={"providers": {"llm": {"mode": "custom", "provider_id": "b7e1…"}}},
)
print(resp.json())

Node

const resp = await fetch("https://api.bithuman.ai/v1/agent/A12345678", {
  method: "POST",
  headers: {
    "api-secret": process.env.BITHUMAN_API_SECRET,
    "content-type": "application/json",
  },
  body: JSON.stringify({ providers: { llm: { mode: "custom", provider_id: "b7e1…" } } }),
});
console.log(await resp.json());
{ "agent_code": "A12345678", "updated": true }

A provider_id you have not registered is rejected with 400 VALIDATION_ERROR naming the id — the selection is never stored half-configured. The change applies to the next session; a call already in progress keeps the provider it started with.

OpenAI-compatible endpoints (self-hosted, proxies, gateways)

Any endpoint that speaks the OpenAI chat-completions API works: register it with platform: "openai" and put your endpoint in credentials.baseUrl.

curl

curl -X PUT https://api.bithuman.ai/v2/$USER_ID/providers \
  -H "api-secret: $BITHUMAN_API_SECRET" -H "content-type: application/json" \
  -d '{"entries":[{
        "platform": "openai",
        "label": "My gateway",
        "credentials": {"apiKey": "…", "baseUrl": "https://llm.example.com/v1"},
        "options": {"llm": {"model": "my-model"}}
      }]}'

Python

import os, requests

resp = requests.put(
    f"https://api.bithuman.ai/v2/{os.environ['USER_ID']}/providers",
    headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
    json={
        "entries": [
            {
                "platform": "openai",
                "label": "My gateway",
                "credentials": {"apiKey": "…", "baseUrl": "https://llm.example.com/v1"},
                "options": {"llm": {"model": "my-model"}},
            },
        ],
    },
)
print(resp.json())

Node

const resp = await fetch(`https://api.bithuman.ai/v2/${process.env.USER_ID}/providers`, {
  method: "PUT",
  headers: {
    "api-secret": process.env.BITHUMAN_API_SECRET,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    entries: [
      {
        platform: "openai",
        label: "My gateway",
        credentials: { apiKey: "…", baseUrl: "https://llm.example.com/v1" },
        options: { llm: { model: "my-model" } },
      },
    ],
  }),
});
console.log(await resp.json());
  • baseUrl is the base, not the full route. Use https://llm.example.com/v1 — we append /chat/completions ourselves. A pasted full endpoint is tolerated and trimmed.
  • options.llm.model is the model name we send in the request body.
  • There is no host allowlist. Any reachable HTTPS endpoint is called as configured; you do not need to ask us to permit a domain.

Knowing which end user a call belongs to

Requests to your endpoint carry the visitor’s identifier in the standard OpenAI user field, so you can attribute a call without any bitHuman-specific parsing:

{
  "model": "my-model",
  "messages": [ … ],
  "stream": true,
  "user": "29830a59917f82805468b3170b0d9082"
}

The value is resolved in this order, and the first one present wins:

  1. the fingerprint your backend passed when it minted the visitor’s embed token with POST /v1/embed-tokens/request;
  2. a per-conversation id that bitHuman generates.

To get a durable value, mint the embed token on your backend with your own id for the end user as its fingerprint, and pass the token to the iframe:

// server: mint one token per visitor session (the api-secret never reaches the browser)
const res = await fetch("https://api.bithuman.ai/v1/embed-tokens/request", {
  method: "POST",
  headers: {
    "api-secret": process.env.BITHUMAN_API_SECRET,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    agent_id: process.env.BITHUMAN_AGENT_CODE, // your agent's code
    fingerprint: visitorId, // your stable id for this end user, the same on every visit
  }),
});
const { data: { token } } = await res.json();
<!-- <AGENT_CODE> = the agent the token was minted for; <TOKEN> = the token from the call above -->
<iframe src="https://www.bithuman.ai/embed/<AGENT_CODE>?token=<TOKEN>" allow="microphone *; camera *; autoplay *" style="width:100%;height:100vh;border:0"></iframe>

Mint the same fingerprint for a returning visitor and your endpoint sees the same user on every call, across sessions. The per-conversation id is the automatic fallback so the field is never empty, but it changes each session — if you see a value that varies per conversation, the embed was opened without a token minted this way.

Two limits worth knowing:

  • Sent only to your own endpoint. Agents on bitHuman’s default LLM do not send it, and neither does the fallback if your endpoint is unreachable.
  • Treat it as a hint, not an authenticated identity. The value is whatever your backend put in the token it minted, so it is only as trustworthy as that backend’s own notion of the visitor. Use it to group and attribute traffic; do not use it alone to authorise access to one user’s data.

Each request also carries X-LiveKit-Room-ID and X-LiveKit-Job-ID headers. Those identify the live session, not the person, and are useful for correlating a single conversation’s calls.