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:
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | yes | Provider id, e.g. openai, elevenlabs, cartesia, grok. |
id | string | no | Existing entry id to update; omit to add a new one. |
label | string | no | Display name; defaults to the platform name. |
credentials | object | no | Key/value credentials. Sensitive keys are encrypted at rest. |
options | object | no | Per-capability configuration. |
clearSecrets | string[] | no | Sensitive credential keys to remove. |
metadata | object | no | Freeform 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.
entriesreplaces 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, useclearSecrets.
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.
| Capability | Value | Meaning |
|---|---|---|
llm / stt / tts / realtime | "default" | Use the bitHuman-managed provider (the default). |
llm / stt / tts / realtime | {"mode": | 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());
baseUrlis the base, not the full route. Usehttps://llm.example.com/v1— we append/chat/completionsourselves. A pasted full endpoint is tolerated and trimmed.options.llm.modelis 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:
- the
fingerprintyour backend passed when it minted the visitor’s embed token withPOST /v1/embed-tokens/request; - 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.