Customer workspaces
A customer workspace is a separate workspace created by a partner on behalf of an end user. It allows you to:
- Manage billing separately per end user
- Isolate data between end users
- Use your own API key to create and control the customer workspace
- Use the
X-Workspace-Idheader to run requests inside a customer workspace
This guide covers the typical flow: create a workspace, fund it from your balance, and run your end user's requests inside it.
Why Use Customer Workspaces
Customer workspaces enable a reseller model: you have one API key, one balance, and many end users.
- Your balance funds all customer workspaces. When you transfer credits to a customer, they come from your account.
- Each customer workspace is isolated. Customer A cannot see Customer B's agents, calls, or data.
- Your API key never touches customer requests. You use your key to manage workspaces; customers use the
X-Workspace-Idheader to run their own requests.
Workspace Hierarchy
Customer workspaces are one level deep:
Your Workspace (Partner)
├── Customer Workspace 1 (End User A)
├── Customer Workspace 2 (End User B)
└── Customer Workspace 3 (End User C)
A customer workspace cannot create its own child workspaces. POST /v1/workspaces always creates a child of your own workspace, regardless of the X-Workspace-Id header (the header is ignored for workspace creation).
Create a Customer Workspace
Use POST /v1/workspaces with your API key:
curl -X POST https://api.dialogbrain.com/api/v1/workspaces \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: acme-user-001" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Support",
"external_id": "acme-user-001"
}'
Response (201 Created)
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": null,
"balance_usd": 0.0
}
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Display name for the workspace (e.g., the end user's company or account name). |
Idempotency-Key header | string | Unique key for this workspace creation. If you retry with the same key, the API returns the existing workspace instead of creating a duplicate. Use your end user's id or a stable identifier. |
Optional Fields
| Field | Type | Description |
|---|---|---|
external_id | string | Your own identifier for this end user (e.g., their customer id in your database). If you retry with the same external_id, you get the existing workspace. Recommended for idempotency in your retries. |
Key: Idempotency-Key
The Idempotency-Key header is required and must be unique for each creation. It prevents duplicate workspaces if your request is retried.
Best practice: Use your end user's id as the idempotency key:
curl -X POST https://api.dialogbrain.com/api/v1/workspaces \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: my-customer-id-12345" \
-d '{"name": "Customer Name", "external_id": "my-customer-id-12345"}'
If the network times out and you retry with the same key, the API returns the existing workspace (201 on first creation, 200 on replay).
Missing Idempotency-Key → 400 Bad Request
{
"detail": "Idempotency-Key header is required — a retried request must never leave a second workspace behind"
}
Create and Deploy a Widget for a Customer
After creating a customer workspace, you can deploy a livechat widget to your end user's website. The widget runs inside the customer workspace, so conversations, billing, and data all stay isolated.
The Order of Operations
A voice widget needs three things: a workspace, an agent, and a trigger that tells the agent to answer voice calls. Here is the sequence:
- Create the customer workspace (done above)
- Transfer credits (done above)
- Create an agent in the workspace (
POST /v1/agentswithX-Workspace-Id) - Attach a trigger to the agent (
POST /v1/agents/{agent_id}/triggers) — this is what makes the agent answer - Create the widget (
POST /v1/widgetswith the same header) - Hand the embed code to your end user
Step 1-2: Workspace and Credits
You already created and funded a workspace above. Now create an agent in it.
Step 3: Create an Agent in the Workspace
curl -X POST https://api.dialogbrain.com/api/v1/agents \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: agent-for-widget-1" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Assistant",
"prompt_text": "You are a helpful support assistant. Answer customer questions about our products."
}'
Response (201 Created)
{
"id": 42,
"name": "Support Assistant",
"prompt_text": "You are a helpful support assistant. Answer customer questions about our products.",
"voice_enabled": false,
"created_at": "2026-08-22T10:30:00Z",
"updated_at": "2026-08-22T10:30:00Z"
}
The agent is created inside workspace 4271 and is unique within that workspace.
Step 4: Attach a Trigger to the Agent
An agent without a trigger is deaf to voice calls. The trigger tells the agent which channel types to answer. For a voice widget, you create an incoming_call trigger with channel_types: ["livechat_voice"].
Use an agent you created in this workspace. GET /v1/agents also lists the platform's shared templates, and attaching a trigger to one of those answers 404. Create the agent first (Step 3), then attach the trigger to the id that call returned.
curl -X POST https://api.dialogbrain.com/api/v1/agents/42/triggers \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Content-Type: application/json" \
-d '{
"trigger_type": "incoming_call",
"conditions": {
"channel_types": ["livechat_voice"]
}
}'
Response (201 Created)
{
"id": 7,
"agent_id": 42,
"trigger_type": "incoming_call",
"conditions": {
"channel_types": ["livechat_voice"]
},
"send_mode": null,
"enabled": true,
"priority": 100,
"created_at": "2026-08-22T10:35:00+00:00",
"updated_at": "2026-08-22T10:35:00+00:00"
}
Scope: This trigger is workspace-level by default — every voice widget in workspace 4271 will route incoming calls to agent 42. If your customer workspace holds only one end user, this is what you want.
If you need to narrow the trigger to a single widget, add channel_account_ids to the conditions:
{
"trigger_type": "incoming_call",
"conditions": {
"channel_types": ["livechat_voice"],
"channel_account_ids": [91]
}
}
This trigger then fires only for widget 91, leaving other widgets in the workspace without an agent.
Why this step matters: Without a trigger, a minted voice token returns 404: "no voice trigger matches widget 92 — attach a voice agent to it first". The trigger is what routes voice calls to the agent.
Step 5: Create the Widget
Now create a widget in the same workspace. The widget needs a name field; all other settings are optional and can be customized later via PATCH.
Simple Chat Widget (No Voice)
curl -X POST https://api.dialogbrain.com/api/v1/widgets \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: widget-support-1" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Chat"
}'
Response (201 Created)
{
"id": 91,
"widget_key": "550e8400-e29b-41d4-a716-446655440000",
"name": "Support Chat",
"is_active": true,
"theme": {
"primaryColor": "#2563eb",
"position": "bottom-right",
"buttonIcon": "chat",
"headerTitle": "Chat with us",
"headerSubtitle": "We typically reply within minutes"
},
"embed_code": "<script>\n window.DialogBrainLiveChat = { widgetKey: \"550e8400-e29b-41d4-a716-446655440000\" };\n</script>\n<script src=\"https://dialogbrain.com/livechat/widget.js\" async></script>",
"allow_voice": false,
"display_mode": "chat",
"created_at": "2026-08-22T10:31:00Z",
"updated_at": "2026-08-22T10:31:00Z"
}
The embed_code field contains the script tag your end user pastes into their website HTML. The widget is ready to use immediately.
Voice-Enabled Widget (Chat + Microphone)
To add voice to the widget, set allow_voice: true:
curl -X POST https://api.dialogbrain.com/api/v1/widgets \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: widget-voice-support-1" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Chat with Voice",
"allow_voice": true
}'
Response:
{
"id": 92,
"widget_key": "660e8400-e29b-41d4-a716-446655440001",
"name": "Support Chat with Voice",
"is_active": true,
"theme": { ... },
"embed_code": "<script>\n window.DialogBrainLiveChat = { widgetKey: \"660e8400-e29b-41d4-a716-446655440001\" };\n</script>\n<script src=\"https://dialogbrain.com/livechat/widget.js\" async></script>",
"allow_voice": true,
"display_mode": "chat",
"created_at": "2026-08-22T10:32:00Z",
"updated_at": "2026-08-22T10:32:00Z"
}
Important: allow_voice: true on a normal display_mode (chat or headless) does NOT require a voice agent — it just adds the microphone button. The agent can handle both text and voice calls seamlessly.
Voice-Only Widget (Microphone Bubble Only)
For a voice-only widget, set display_mode: "voice_only" and allow_voice: true. This requires a voice agent already attached to the workspace:
curl -X POST https://api.dialogbrain.com/api/v1/widgets \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: widget-voice-only-1" \
-H "Content-Type: application/json" \
-d '{
"name": "Quick Call Button",
"display_mode": "voice_only",
"allow_voice": true,
"voice_button_label": "Call us now"
}'
Response:
{
"id": 93,
"widget_key": "770e8400-e29b-41d4-a716-446655440002",
"name": "Quick Call Button",
"is_active": true,
"theme": { ... },
"embed_code": "<script>\n window.DialogBrainLiveChat = { widgetKey: \"770e8400-e29b-41d4-a716-446655440002\" };\n</script>\n<script src=\"https://dialogbrain.com/livechat/widget.js\" async></script>",
"allow_voice": true,
"display_mode": "voice_only",
"localization": {
"voice_button_label": "Call us now"
},
"created_at": "2026-08-22T10:33:00Z",
"updated_at": "2026-08-22T10:33:00Z"
}
Critical constraint: display_mode: "voice_only" requires both:
allow_voice: true- A voice agent already attached to the workspace
If either is missing, the create fails with a 400 error:
# Missing allow_voice: true
curl -X POST https://api.dialogbrain.com/api/v1/widgets \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: widget-voice-only-bad" \
-d '{
"name": "Bad Voice Widget",
"display_mode": "voice_only"
}'
Response:
{
"detail": {
"error": "voice_only widgets require allow_voice=true",
"error_type": "voice_only_requires_voice"
}
}
Status: 400 Bad Request. Branch on detail.error_type, not on the message text.
If no agent is attached:
{
"detail": {
"error": "no voice agent attached to this workspace; attach one before switching to voice_only",
"error_type": "voice_only_requires_workspace_agent"
}
}
Status: 400 Bad Request
Step 6: Customization via PATCH
Create a simple widget first, then customize it later with PATCH /v1/widgets/{widget_id}. Only changed fields need to be sent:
curl -X PATCH https://api.dialogbrain.com/api/v1/widgets/91 \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Content-Type: application/json" \
-d '{
"allow_voice": true,
"primary_color": "#10b981"
}'
Step 7: Hand Over the Embed Code
Retrieve the embed code from the response or fetch it separately:
curl -X GET https://api.dialogbrain.com/api/v1/widgets/91/embed-code \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271"
Response:
{
"widget_id": 91,
"widget_name": "Support Chat",
"widget_key": "550e8400-e29b-41d4-a716-446655440000",
"is_active": true,
"embed_code": "<script>\n window.DialogBrainLiveChat = { widgetKey: \"550e8400-e29b-41d4-a716-446655440000\" };\n</script>\n<script src=\"https://dialogbrain.com/livechat/widget.js\" async></script>"
}
Your end user pastes the embed_code into their website's HTML (typically in the footer or <head> tag). The widget is then live on their site.
Deleting a Widget
Important: Deleting a widget is permanent and irreversible.
curl -X DELETE https://api.dialogbrain.com/api/v1/widgets/91 \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271"
Response: 204 No Content
When you delete a widget:
- Every conversation the widget produced is deleted
- Every message in those conversations is deleted
- The embed code stops working immediately
- Data cannot be recovered
Before deleting a widget, export any conversations you need to keep.
Run Requests Inside a Customer Workspace
Once a customer workspace is created, use the X-Workspace-Id header to run any v1 API request inside it.
Example: Create an Agent
curl -X POST https://api.dialogbrain.com/api/v1/agents \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Content-Type: application/json" \
-d '{
"name": "Sales Assistant",
"prompt_text": "You are a helpful sales assistant."
}'
The agent is created inside workspace 4271, not in your own workspace. The response includes the agent id, which is unique within that workspace.
Example: Make a Voice Call
curl -X POST https://api.dialogbrain.com/api/v1/calls \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-d '{
"agent_id": 42,
"target": "+1 555 0100"
}'
The call is made with the agent inside workspace 4271. Credit usage is deducted from workspace 4271's balance.
Which Endpoints Accept X-Workspace-Id
All /v1/* endpoints accept the X-Workspace-Id header. This includes:
POST /v1/agents— create an agentGET /v1/agents— list agentsPOST /v1/calls— make a callGET /v1/calls— list callsPOST /v1/threads— create a thread- And all other v1 routes
The only exception is POST /v1/workspaces itself (creation), which always creates a child of your workspace and ignores any X-Workspace-Id header.
Errors with X-Workspace-Id
| Status | Reason |
|---|---|
404 Not Found | The workspace id does not exist, or you do not own it. |
403 Forbidden | The workspace exists but is suspended. |
Example: if you use a workspace id you do not own:
curl -X POST https://api.dialogbrain.com/api/v1/agents \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 9999" \
-d '{"name": "Agent"}'
Response:
{
"detail": "Workspace not found"
}
Status: 404 Not Found
Fund a Customer Workspace
Use POST /v1/workspaces/{workspace_id}/credits to transfer credits from your balance to a customer workspace.
curl -X POST https://api.dialogbrain.com/api/v1/workspaces/4271/credits \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: topup-acme-1" \
-H "Content-Type: application/json" \
-d '{
"amount_usd": "50.00"
}'
Response (200 OK)
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": null,
"balance_usd": 50.0
}
The balance_usd field shows the workspace's new balance after the transfer.
Required Fields
| Field | Type | Description |
|---|---|---|
amount_usd | string | Amount to transfer in USD as a decimal string (e.g., "50.00"). Must be positive and convert to whole microdollars (6 decimal places). |
Idempotency-Key header | string | Unique key for this transfer. Prevents double-charging if your request is retried. |
Balance Deduction
Credits are deducted from your workspace balance and added to the customer workspace's balance. If your balance is insufficient:
curl -X POST https://api.dialogbrain.com/api/v1/workspaces/4271/credits \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: topup-acme-2" \
-d '{"amount_usd": "10000.00"}'
Response (409 Conflict):
{
"detail": "Parent workspace has insufficient balance"
}
Status: 409 Conflict
Errors
| Status | Condition |
|---|---|
400 Bad Request | Missing Idempotency-Key header. |
404 Not Found | Workspace id does not exist or is not owned by you. |
403 Forbidden | The workspace is suspended. |
409 Conflict | Your balance is insufficient, or an idempotency conflict occurred. |
422 Unprocessable Entity | amount_usd is invalid (not a valid decimal, non-positive, or converts to non-integral microdollars). |
Example: invalid amount format:
curl -X POST https://api.dialogbrain.com/api/v1/workspaces/4271/credits \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: topup-acme-3" \
-d '{"amount_usd": "not-a-number"}'
Response (422 Unprocessable Entity):
{
"detail": "amount_usd must be a valid decimal string"
}
Example: non-integral microdollars (0.123456789 USD):
curl -X POST https://api.dialogbrain.com/api/v1/workspaces/4271/credits \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Idempotency-Key: topup-acme-4" \
-d '{"amount_usd": "10.1234567"}'
Response (422 Unprocessable Entity):
{
"detail": "amount_usd converts to non-integral microdollars"
}
Suspend a Customer Workspace
Instead of deleting a workspace (which is not supported), you can suspend it:
curl -X PATCH https://api.dialogbrain.com/api/v1/workspaces/4271 \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"suspended": true
}'
Response (200 OK)
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": "2026-08-22T12:30:00Z",
"balance_usd": 50.0
}
The suspended_at field shows when the workspace was suspended. While suspended:
- No API requests can run inside the workspace (they return
403 Forbidden) - Credits cannot be added (they return
403 Forbidden) - The workspace remains visible in listings and fetch operations
Resume a Workspace
To resume a suspended workspace, set suspended: false:
curl -X PATCH https://api.dialogbrain.com/api/v1/workspaces/4271 \
-H "X-API-Key: db_live_YOUR_KEY" \
-d '{
"suspended": false
}'
Response:
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": null,
"balance_usd": 50.0
}
The suspended_at field is set to null, and the workspace is now active.
Rename a Workspace
Update the name field:
curl -X PATCH https://api.dialogbrain.com/api/v1/workspaces/4271 \
-H "X-API-Key: db_live_YOUR_KEY" \
-d '{
"name": "Acme Support (Premium)"
}'
List and Fetch Customer Workspaces
List All Your Customer Workspaces
curl https://api.dialogbrain.com/api/v1/workspaces \
-H "X-API-Key: db_live_YOUR_KEY"
Response (200 OK)
{
"items": [
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": null,
"balance_usd": 50.0
},
{
"id": 4272,
"name": "Tech Startup",
"external_id": "startup-123",
"created_at": "2026-08-22T11:00:00Z",
"suspended_at": null,
"balance_usd": 25.5
}
],
"total": 2,
"limit": 50,
"offset": 0
}
Pagination
Add limit and offset query parameters:
curl "https://api.dialogbrain.com/api/v1/workspaces?limit=10&offset=20" \
-H "X-API-Key: db_live_YOUR_KEY"
limit: Number of items to return (default: 50, max: 200)offset: Number of items to skip (default: 0)
Fetch One Workspace
curl https://api.dialogbrain.com/api/v1/workspaces/4271 \
-H "X-API-Key: db_live_YOUR_KEY"
Response (200 OK)
{
"id": 4271,
"name": "Acme Support",
"external_id": "acme-user-001",
"created_at": "2026-08-22T10:00:00Z",
"suspended_at": null,
"balance_usd": 50.0
}
Using the Python SDK
The dialogbrain SDK provides helper methods for managing customer workspaces.
Install the SDK
pip install dialogbrain
Create a Workspace
from dialogbrain import SyncDialogBrainClient
client = SyncDialogBrainClient(api_key="db_live_YOUR_KEY")
# Create a customer workspace
workspace = client.workspaces.create(
name="Acme Support",
external_id="acme-user-001",
idempotency_key="acme-user-001"
)
print(f"Workspace ID: {workspace.id}")
print(f"Balance: ${workspace.balance_usd}")
Fund a Workspace
# Top up a customer workspace
workspace = client.workspaces.topup(
workspace_id=4271,
amount_usd="50.00",
idempotency_key="topup-acme-1"
)
print(f"New balance: ${workspace.balance_usd}")
Run Requests Inside a Workspace
Use the workspace_id parameter to run requests inside a customer workspace:
# Create an agent inside workspace 4271
agent = client.agents.create(
name="Sales Assistant",
prompt_text="You are a helpful sales assistant.",
workspace_id=4271
)
# Make a call inside workspace 4271
call = client.calls.create(
agent_id=agent.id,
target="+1 555 0100",
workspace_id=4271
)
List and Fetch Workspaces
# List all customer workspaces
workspaces = client.workspaces.list(limit=50, offset=0)
for ws in workspaces.items:
print(f"{ws.name}: ${ws.balance_usd}")
# Fetch one workspace
workspace = client.workspaces.get(workspace_id=4271)
print(f"Workspace: {workspace.name}")
Suspend and Resume
# Suspend a workspace
workspace = client.workspaces.update(
workspace_id=4271,
suspended=True
)
# Resume a workspace
workspace = client.workspaces.update(
workspace_id=4271,
suspended=False
)
Mint a Voice Token for an End User
Your end user's client application needs a LiveKit room and token to make a voice call. Your server mints these credentials and hands only the token to the client — the client never holds an API key.
The partner's server calls POST /v1/voice/tokens with your API key and the customer workspace ID. The route validates that the widget exists, has voice enabled, and has a voice trigger attached to the agent. You get back a room name, token, and an expiry time. The client uses the token to connect to LiveKit.
Every voice call spends credits from the customer workspace's balance, not your own — this is the core of the reseller billing model. If you run out of credits in a customer workspace, calls fail.
Request
curl -X POST https://api.dialogbrain.com/api/v1/voice/tokens \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: call-visitor-alice-001" \
-H "Content-Type: application/json" \
-d '{
"widget_id": 92,
"end_user_id": "visitor-alice-12345",
"metadata": {
"visitor_name": "Alice",
"visitor_email": "alice@example.com"
}
}'
Required Headers
| Header | Description |
|---|---|
X-API-Key | Your API key. |
X-Workspace-Id | The workspace id of the customer. |
Idempotency-Key | Unique key for this request. If the request is retried, you get the same room back instead of starting a second call. Use a stable identifier like visitor-{id} or include a timestamp. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
widget_id | integer | yes | The id of the widget in the customer workspace. Must have allow_voice: true and a voice trigger attached to the workspace's agent (see Step 4 above). |
end_user_id | string | no | Your identifier for the person calling (e.g., a visitor id or session id). If you omit it, one is auto-generated. It arrives on the call events as metadata.end_user_id. |
metadata | object | no | Your own correlation data (e.g., session context, tracking ids). Stored with the call and returned on the call events under metadata, alongside end_user_id. It is never shown to the agent. |
Response (200 OK)
{
"url": "ws://livekit.dialogbrain.com/room?token=eyJ...",
"token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"room": "acme-widget-92-call-visitor-alice-12345",
"call_id": "c63f2b8a-1234-5678-90ab-cdef12345678",
"expires_at": "2026-08-23T11:30:00+00:00"
}
Response Fields
| Field | Type | Description |
|---|---|---|
url | string | The LiveKit connection URL (WebSocket). Hand this and the token to your client. |
token | string | The LiveKit token. Your client includes it in the connection handshake. |
room | string | The room name inside LiveKit. |
call_id | string | A unique identifier for this call session. Use it to link the call to your own records. |
expires_at | string | ISO 8601 timestamp. The token and room expire at this time. After expiry, request a new token. |
Expiry and Renewal
The token has a fixed lifetime (typically 1 hour). When a call ends or the token is about to expire, the client can request a fresh token with a new Idempotency-Key to reconnect to a new room.
Client-Side Integration
Your client receives url and token and connects to LiveKit. The exact connection and media handling is LiveKit's responsibility — see LiveKit JavaScript SDK. The call_id is useful for your own analytics and debugging.
Call Events
After the token is minted you receive a call.started event whose metadata carries your correlation data and end_user_id. The call lifecycle is complete — you see call.started, then call.ended when the caller hangs up or the session times out.
Errors
400 Bad Request: Missing Idempotency-Key
curl -X POST https://api.dialogbrain.com/api/v1/voice/tokens \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Content-Type: application/json" \
-d '{"widget_id": 92}'
Response:
{
"detail": "Idempotency-Key header is required — a retried request must never start a second call"
}
403 Forbidden: Voice Disabled on Widget
curl -X POST https://api.dialogbrain.com/api/v1/voice/tokens \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: call-visitor-bob-001" \
-d '{"widget_id": 91}' # This widget has allow_voice: false
Response:
{
"detail": "widget 91 has voice turned off"
}
403 Forbidden: Voice Disabled for Workspace
{
"detail": "voice is disabled for this workspace"
}
404 Not Found: Widget Does Not Exist
curl -X POST https://api.dialogbrain.com/api/v1/voice/tokens \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: call-visitor-carol-001" \
-d '{"widget_id": 9999}' # Widget id does not exist
Response:
{
"detail": "no widget 9999 in this workspace"
}
404 Not Found: No Voice Trigger Attached to Widget
curl -X POST https://api.dialogbrain.com/api/v1/voice/tokens \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: call-visitor-dave-001" \
-d '{"widget_id": 92}' # Widget exists but has no voice trigger attached
Response:
{
"detail": "no voice trigger matches widget 92 — attach a voice agent to it first"
}
This error means you skipped Step 4 (attaching a trigger to the agent). Go back and POST /v1/agents/{agent_id}/triggers with trigger_type: "incoming_call" and conditions: {"channel_types": ["livechat_voice"]}.
429 Too Many Requests: Rate Limited
{
"detail": "rate limit exceeded",
"retry_after": 60
}
The Retry-After header indicates how many seconds to wait before retrying.
Bringing the Agent Into YOUR LiveKit Room
The previous section (/v1/voice/tokens) covers the pattern where your client joins our LiveKit room. That works for a livechat widget.
Use POST /v1/voice/bridge when you have your own LiveKit room and want our agent to join yours. This pattern is common in enterprise integrations where you host the room infrastructure and want the agent as a participant.
When to Use Each
POST /v1/voice/tokens— Your client connects to our LiveKit room. Best for: widgets, web apps, simple voice calls.POST /v1/voice/bridge— Our agent connects to your LiveKit room. Best for: custom integrations, multi-party rooms you control, existing LiveKit infrastructure.
The Bridge Request
curl -X POST https://api.dialogbrain.com/api/v1/voice/bridge \
-H "X-API-Key: db_live_YOUR_KEY" \
-H "X-Workspace-Id: 4271" \
-H "Idempotency-Key: bridge-call-xyz-789" \
-H "Content-Type: application/json" \
-d '{
"livekit_url": "wss://livekit.example.com",
"room": "collab-session-001",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"agent_id": 42,
"caller_external_identity": "participant-alice-001",
"answer_delay_s": 15,
"metadata": {
"session_id": "sess_12345",
"customer": "Acme Inc"
}
}'
Required Fields
| Field | Type | Description |
|---|---|---|
livekit_url | string | WebSocket URL of your LiveKit instance (wss:// or ws://). Must not point to private IPs or IMDS. |
room | string | Room name in your LiveKit instance. |
token | string | LiveKit access token for the agent to join your room. Must have publish and subscribe permissions on that room. |
agent_id | integer | The DialogBrain agent to handle the call. |
caller_external_identity | string | The participant identity of the caller in your room — who we listen to and whose departure ends the call. |
Optional Fields
| Field | Type | Default | Description |
|---|---|---|---|
answer_delay_s | integer | 0 | Seconds to ring before the agent answers (0-120). Useful for ring-first workflows where a human gets a chance to pick up first. If set, the agent will not join the room until the delay expires or an owner_identity joins. |
metadata | object | null | Arbitrary JSON data (max 4 KB). Returned on call.started and call.ended events. |
Response (200 OK)
{
"call_id": "ext-bridge-a1b2c3d4e5f6",
"room": "agent-livekit-room-001",
"status": "active"
}
Or, if answer_delay_s > 0:
{
"call_id": "ext-bridge-x9y8z7w6v5u4",
"room": null,
"status": "ringing"
}
Response Fields
| Field | Type | Description |
|---|---|---|
call_id | string | Unique call identifier. Use it to fetch call details or to read the call transcript later. |
room | string or null | The internal room name where the agent joined (after the delay, if any). While status == "ringing" and a delay is active, room is null. Once the agent answers, room is filled in. |
status | string | Either "ringing" (waiting for delay or owner to join) or "active" (agent is in the room). |
Token Requirements
The token you provide must:
- Have
publishandsubscribepermissions on the room — the agent needs to send and receive audio. - Outlast the call — include an expiry (
exp) that is at leastmax(answer_delay_s, 300)seconds in the future. If the token expires while the call is active, the agent will lose connection and the bridge will end.
Example: if answer_delay_s: 60, the token's remaining lifetime must be at least 60 seconds.
Call Lifecycle
The call lifecycle uses the standard routes and events:
- Hangup —
DELETE /v1/calls/{call_id}(with your API key and the customer workspace header). - Read the call —
GET /v1/calls/{call_id}returns the call status, duration, transcript, and any metadata you provided. - Call events — You receive
call.startedandcall.endedwebhook events (if configured) with yourmetadataattached. Thecall_idin the event matches the response above.
See the REST API reference and Webhooks for full details.
Caller Identity and Ring Delays
caller_external_identity— The participant whose audio we listen to. If this participant leaves the room, we hang up and mark the call asoutcome: "caller_left". The agent will not speak until this participant is present and listening.owner_identity(optional, a field of this request) — the room owner's participant identity, meaningful only withanswer_delay_s > 0. The owner joining means the human answered the call themselves: the agent STANDS DOWN, the bridge leaves quietly, and nothing is billed. This is the concierge pattern — the agent is the fallback, never a competitor for the call.end_user_id(optional) — your identifier for the caller; it comes back on the call events insidemetadata.
The ring-first pattern:
- Send
answer_delay_s: 120and the owner's participant identity asowner_identity. - If the owner joins the room within 120 seconds, the call is theirs — the agent never speaks and you are not billed.
- If the owner does not join in time, the agent answers automatically.
Errors
| Status | Reason |
|---|---|
400 | Missing Idempotency-Key header. |
403 | Voice is disabled for this workspace. |
404 | Agent not found in this workspace. |
409 | Idempotency-Key already in use or conflict detected. Retry with Retry-After header. |
422 | SSRF validation failed, token expired or missing fields, or answer_delay_s out of range. |
429 | Bridge capacity exhausted. Retry with Retry-After header. |
502 | Failed to dispatch agent or connect to partner room. |
Client-Side Integration
Your client joins your own LiveKit room using your own SDK and credentials. The agent (our participant) will join as extbridge-{call_id} and start listening and speaking. For LiveKit client documentation, see LiveKit Documentation.
See also: Workspaces, Idempotency, Errors