Webhooks
Receive signed notifications when async work finishes, and verify each signature.
Two kinds of notification share this page:
- Job webhooks (agent ready, video done): registered through the API and signed. Most of this page.
- Session events (each message of a live chat): set in the Console and not signed.
Agent generation and talking-video renders are asynchronous: agent creation takes
minutes for the first-generation models and Essence 2. Expression 2 takes longer
to build. Instead of polling GET /v1/agent/status/{id} or
GET /v1/video/{job_id}, register a webhook and bitHuman will POST a
signed event to your endpoint the moment the work finishes.
Events
| Event | Fires when |
|---|---|
agent.ready | An agent finished generating and is ready to use. |
agent.failed | Agent generation failed (data.error has the reason). |
video. | A POST / render finished. data has the same shape as GET /: job_id, status, model, video_url, duration_, credits_. |
video.failed | A render failed. data. has the reason, and the charge is refunded. |
ping | Sent only by POST /. |
More event types will be added over time. Subscribe to a subset, or omit
events (or pass []) to receive all of them.
Register an endpoint
POST /v1/webhooks createWebhook
Keep the id for the calls below: export WEBHOOK_ID=<data.id from the response>.
The
secretis returned only once. Store it — it signs every delivery and is redacted from all later responses.
curl
curl -X POST https://api.bithuman.ai/v1/webhooks \
-H "api-secret: $BITHUMAN_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/bithuman/webhook",
"events": ["agent.ready", "agent.failed"],
"description": "prod"
}'
Python
import os, requests
resp = requests.post(
"https://api.bithuman.ai/v1/webhooks",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
json={
"url": "https://example.com/bithuman/webhook",
"events": ["agent.ready", "agent.failed"],
"description": "prod",
},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks", {
method: "POST",
headers: {
"api-secret": process.env.BITHUMAN_API_SECRET,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://example.com/bithuman/webhook",
events: ["agent.ready", "agent.failed"],
description: "prod",
}),
});
console.log(await resp.json());
{
"success": true,
"data": {
"id": "f2cd30a2-…",
"url": "https://example.com/bithuman/webhook",
"events": ["agent.ready", "agent.failed"],
"active": true,
"secret": "whsec_60fe3d…"
}
}What a delivery looks like
POST /bithuman/webhook HTTP/1.1
Content-Type: application/json
X-BitHuman-Event: agent.ready
X-BitHuman-Delivery: evt_a1b2c3…
X-BitHuman-Timestamp: 1780417371
X-BitHuman-Signature: sha256=9f86d081…
{"id":"evt_a1b2c3…","event":"agent.ready","created":1780417371,"data":{"agent_id":"A80HVD8577","code":"A80HVD8577"}}
Verify the signature
Compute HMAC-SHA256 over "{timestamp}.{raw_body}" using your secret and
compare it — in constant time — to the hex digest in X-BitHuman-Signature.
Reject anything older than a few minutes to prevent replays.
import hashlib, hmac, time
def verify(secret: str, headers, raw_body: bytes) -> bool:
ts = headers["X-BitHuman-Timestamp"]
if abs(time.time() - int(ts)) > 300: # 5-minute replay window
return False
sent = headers["X-BitHuman-Signature"].removeprefix("sha256=")
expected = hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(sent, expected)
Always verify against the raw request body — re-serializing the JSON will change the bytes and break the signature.
Delivery & retries
- Respond
2xxquickly (within 10s). Do heavy work asynchronously. - Each delivery is attempted up to 3 times (immediately, then after 2 s and 5 s); each attempt times out after 10 s.
- Every attempt is logged. Inspect recent attempts:
curl
curl https://api.bithuman.ai/v1/webhooks/$WEBHOOK_ID/deliveries \
-H "api-secret: $BITHUMAN_API_SECRET"
Python
import os, requests
resp = requests.get(
f"https://api.bithuman.ai/v1/webhooks/{os.environ['WEBHOOK_ID']}/deliveries",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch(`https://api.bithuman.ai/v1/webhooks/${process.env.WEBHOOK_ID}/deliveries`, {
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
Session events
Separately from the account webhooks above, bitHuman can POST each live conversation to a URL of yours as it happens: once when a visitor connects, then once for every message, the visitor’s and the agent’s.
Set it up
In the Console, open Developer → Webhooks (www.bithuman.ai/developer?tab=webhooks):
- Switch webhooks on. The fields appear once it’s enabled.
- Enter your Endpoint URL. It must be a public
https://URL; private and local addresses are refused. - Add Authentication Headers, e.g.
X-Webhook-Secret: <a long random value>. Deliveries are not signed, so check this header in your handler. - Under Event Subscriptions, turn on
room.joinand/orchat.push. - Save settings, then Test to send a sample delivery.
The setting belongs to an account, not to one agent. An embed of your agent always reports to you, the owner. A conversation on www.bithuman.ai reports to the account signed in there. So when you chat with your own agent, the events come to you. When another signed-in user chats with it, they go to that user’s setting instead. Sessions you host yourself with the SDK, including cloud-avatar sessions driven by your own agent, send no session events.
What a delivery looks like
Each event is an HTTP POST with Content-Type: application/json and your
headers. timestamp values are Unix seconds with a fractional part.
room.join fires once, when the conversation starts.
participant_count is the number of participants in the room at that moment.
Depending on the model, that can already include the agent and its avatar.
{ "agent_id": "A80HVD8577", "event_type": "room.join",
"data": { "room_name": "room-A80HVD8577-qtrJ-cOFR", "participant_count": 3,
"session_id": "8d947f06-2c36-4d98-aafe-b232da2433ff" },
"timestamp": 1790607253.5575945 }
chat.push fires once per completed message. role is "user" for what
the visitor said or typed (the speech transcript, for voice) and "assistant"
for what the agent said, including its greeting.
{ "agent_id": "A80HVD8577", "event_type": "chat.push",
"data": { "role": "assistant", "message": "I'm Pip, your friendly science fox!",
"session_id": "8d947f06-2c36-4d98-aafe-b232da2433ff",
"timestamp": 1790607268.8956978 },
"timestamp": 1790607268.895705 }
agent_id is the agent’s code. Group messages by session_id and order them
by data.timestamp. Each message arrives once it’s finished, not word by word.
A reply the visitor interrupts arrives as far as it got.
Two cases where session_id does not map one-to-one to a conversation:
- If a session restarts mid-conversation, it gets a new
session_idwith no newroom.join. - A kiosk that resets between visitors keeps one
room.joinand onesession_idfor every visitor.
Delivery
- There is one attempt per event, with a 10-second timeout, and no retries.
- Answer exactly
200. Anything else, including201or204, counts as a failure and is not re-sent. - Point the URL straight at your handler. A redirect can turn the POST into a GET and drop the body.
- Answer fast. The conversation waits for your
room.joinresponse before it starts, so a slow endpoint delays the avatar’s first words by up to 10 seconds. Return200at once and do the work on a queue. - Test sends
{"event_type": "test", "agent_id": "test_agent_123", ...}with your headers plusX-Test-Webhook: true.
List webhooks
GET /v1/webhooks listWebhooks
List your registered webhooks (signing secrets are redacted).
curl
curl https://api.bithuman.ai/v1/webhooks \
-H "api-secret: $BITHUMAN_API_SECRET"
Python
import os, requests
resp = requests.get(
"https://api.bithuman.ai/v1/webhooks",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks", {
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
"success": true,
"data": [
{
"url": "https://example.com/bithuman/webhook",
"events": [
"agent.ready",
"agent.failed"
],
"active": true
}
]
}Get a webhook
GET /v1/webhooks/{webhook_id} getWebhook
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
webhook_id | path | string (uuid) | yes |
curl
curl https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182 \
-H "api-secret: $BITHUMAN_API_SECRET"
Python
import os, requests
resp = requests.get(
"https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182", {
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
"data": {
"url": "https://example.com/bithuman/webhook",
"events": [
"agent.ready",
"agent.failed"
],
"active": true
}
}Delete a webhook
DELETE /v1/webhooks/{webhook_id} deleteWebhook
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
webhook_id | path | string (uuid) | yes |
curl
curl -X DELETE https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182 \
-H "api-secret: $BITHUMAN_API_SECRET"
Python
import os, requests
resp = requests.delete(
"https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182", {
method: "DELETE",
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
"success": true,
"deleted": true
}Send a test event
POST /v1/webhooks/{webhook_id}/test testWebhook
Deliver a one-off ping event to verify the endpoint is reachable.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
webhook_id | path | string (uuid) | yes |
curl
curl -X POST https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/test \
-H "api-secret: $BITHUMAN_API_SECRET"
# delivers a one-off "ping" event to your endpoint
Python
import os, requests
resp = requests.post(
"https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/test",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/test", {
method: "POST",
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
"data": {
"delivered": true,
"status_code": 200,
"attempts": 1
}
}List delivery attempts
GET /v1/webhooks/{webhook_id}/deliveries listWebhookDeliveries
Recent delivery attempts for a webhook (most recent first).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
webhook_id | path | string (uuid) | yes |
Every webhook operation is also listed in the API reference index.
curl
curl https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/deliveries \
-H "api-secret: $BITHUMAN_API_SECRET"
Python
import os, requests
resp = requests.get(
"https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/deliveries",
headers={"api-secret": os.environ["BITHUMAN_API_SECRET"]},
)
print(resp.json())
Node
const resp = await fetch("https://api.bithuman.ai/v1/webhooks/1de0c020-9d5e-4f8b-a1c2-3d4e5f607182/deliveries", {
headers: { "api-secret": process.env.BITHUMAN_API_SECRET },
});
console.log(await resp.json());
{
"data": [
{
"event_type": "agent.ready",
"status_code": 200,
"attempts": 1,
"delivered": true
}
]
}This section moved: docs.bithuman.ai/api/webhooks#list-webhooks