Base URL
https://www.bidii.ai/api/v2Start
Your key, the base URL and seven calls from listing your agents to reading this month's usage, then the rules every call follows.
Create a key in Settings > Developers (the Team plan and up) and send it as a bearer token. A key belongs to your workspace, carries only the scopes you gave it and can be revoked at any moment. bidi keeps a hash of it, which is why the dashboard shows it once.
https://www.bidii.ai/api/v2Authorization: Bearer bidi_sk_YOUR_KEYCreate a key in your dashboard
Seven calls, in the order you would make them. Replace the key and the ids with your own; every example is complete.
curl https://www.bidii.ai/api/v2/agents \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"{
"data": [
{
"id": "6f1c0b2e-6b0f-4b7e-9d5a-1b2c3d4e5f60",
"name": "Al Noor Dental",
"status": "live",
"language": "bilingual",
"public_key": "pk_live_ab12cd34",
"allowed_origins": ["https://alnoordental.ae"],
"conversations_per_month": 1500,
"created_at": "2026-09-14T08:12:30+00:00",
"updated_at": "2026-09-30T17:40:02+00:00"
}
],
"next_cursor": null
}curl -X POST https://www.bidii.ai/api/v2/agents/AGENT_ID/sources \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: opening-hours-2026-10" \
-d '{
"type": "text",
"name": "Opening hours",
"text": "Open Saturday to Thursday, 9am to 6pm. Closed on Fridays. Emergencies: +971 4 000 0000."
}'Indexing runs in the background: the answer is 202 with the source at status uploaded. Poll it until indexed. The Idempotency-Key makes a retry safe.
curl -X POST https://www.bidii.ai/api/v2/agents/AGENT_ID/chat \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Are you open on Friday?"}]}'{
"conversation_id": "0c9e7d2a-1f4b-4a63-8e2d-7b5f6a1c2d3e",
"message": {"id": "f3a1...", "role": "assistant", "content": "We are closed on Fridays. Saturday to Thursday we are open 9am to 6pm."},
"outcome": "answered",
"refused": false,
"sources": [{"file_id": "...", "filename": "Opening hours"}],
"state": "ai_active",
"lead_captured": false,
"limit": null,
"degraded": false
}curl -N -X POST https://www.bidii.ai/api/v2/agents/AGENT_ID/chat \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"stream": true,
"conversation_id": "0c9e7d2a-1f4b-4a63-8e2d-7b5f6a1c2d3e",
"messages": [{"role": "user", "content": "And on Saturday?"}]
}'event: delta
data: {"text": "On Saturday we are open "}
event: delta
data: {"text": "from 9am to 6pm."}
event: sources
data: {"sources": [{"file_id": "...", "filename": "Opening hours"}]}
event: outcome
data: {"outcome": "answered", "state": "ai_active", "refused": false, "limit": null}
event: done
data: {"conversation_id": "0c9e7d2a-...", "message": {"id": "...", "role": "assistant", "content": "On Saturday we are open from 9am to 6pm."}, "outcome": "answered", "refused": false, "sources": [...], "state": "ai_active", "lead_captured": false, "limit": null, "degraded": false}Streaming sends the same events the website widget receives: delta pieces of text, then sources, outcome and done. Anything that can refuse the call answers before the stream starts, as a normal HTTP error.
curl "https://www.bidii.ai/api/v2/agents/AGENT_ID/conversations?limit=20" \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"
curl "https://www.bidii.ai/api/v2/agents/AGENT_ID/conversations?limit=20&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"
curl https://www.bidii.ai/api/v2/conversations/CONVERSATION_ID/messages \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"curl "https://www.bidii.ai/api/v2/leads?status=new" \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"
curl -X PATCH https://www.bidii.ai/api/v2/leads/LEAD_ID \
-H "Authorization: Bearer bidi_sk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "qualified"}'curl https://www.bidii.ai/api/v2/usage \
-H "Authorization: Bearer bidi_sk_YOUR_KEY"Lists take limit (1 to 100) and cursor and answer data plus next_cursor; null means you reached the end. Cursors are opaque: pass them back unchanged.
Every error is one shape: an error object with a code, a message and a request_id. Codes are stable snake_case strings to switch on; messages are for people, in English or, with Accept-Language: ar, in Arabic. Quote the request id when you write to us.
Sixty requests per key per minute, twenty of them chat. Over the limit you get 429 with Retry-After; X-RateLimit-Remaining says how many calls are left in the minute.
Send an Idempotency-Key header on a POST that creates something. The same key on the same call within 24 hours returns the first answer, marked Idempotent-Replayed, instead of creating a second thing.
Changes inside v2 only add fields and endpoints; a breaking change becomes v3. Every response carries X-Bidi-Api-Version with the date of the contract you are on.
A conversation started through the API counts against the same monthly allowance as the website widget, on its first answered message. When the month is full the agent answers with its capacity note and the response says limit: monthly.
HTTP/1.1 429 Too Many Requests
Retry-After: 41
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-Bidi-Api-Version: 2026-10-01
{"error": {"code": "rate_limited", "message": "Rate limit reached. Wait for Retry-After seconds and try again.", "request_id": "9f2c1a7d3b4e5f60"}}| Code | Status | Meaning |
|---|---|---|
missing_key | 401 | No Authorization header, or not a bearer token. |
invalid_key | 401 | The key is unknown or was revoked. |
expired_key | 401 | The key passed its expiry date. |
plan_required | 402 | An action past the plan's limit per agent, or one that waits for approval on a plan without the helpdesk; the body carries the plan to move to, except on the plan that holds the most actions, where the way out is removing one. |
plan_required | 403 | The workspace's plan has no API; the body carries the plan to move to. |
insufficient_scope | 403 | The key lacks the scope; required_scope names it. |
secret_key_required | 403 | A publishable key called something other than chat; use a secret key on your server. |
not_found | 404 | No such thing in this workspace, including ids that belong to someone else. |
conversation_expired | 410 | The conversation's 24 hours are over; start a new one. |
validation_error | 422 | The body or query did not validate; details lists each field. |
rate_limited | 429 | Too many calls this minute; wait Retry-After seconds. |
Read API keys and scopes before you ship a key anywhere, the API reference for every endpoint, and Webhooks to hear about leads and conversations the moment they happen.