Core resources
Human handoff
Everything that happens after a chatbot escalates to a person: who is on duty, who gets the conversation, how long the customer waits, and what the operator can do with it. Built so another application — a CRM, a helpdesk, your own dashboard — can be the agent console while Bolcho runs the queue.
The shape of it
Your app owns the people. Bolcho owns the queue. You push a roster of operators and a heartbeat; Bolcho decides who gets the next conversation, measures how long customers waited, returns work when someone disappears, and keeps an audit trail. Your app renders the inbox and calls the actions below.
1 — Register your operators
POST /inbox/operators/sync replaces the roster. Bolcho cannot know who was hired or who is on shift, so this is the one thing it must be told. Operators absent from a sync are marked unavailable, never deleted — a partial sync degrades to "nobody is available" rather than a wiped team and a broken audit trail. Then call POST /inbox/operators/heartbeat every ~30s from each open inbox.
2 — Choose how conversations are routed
Per chatbot, via PATCH /agents/{id}:
- manual — lands in a shared queue for someone to claim (the default)
- round_robin — the eligible operator who has gone longest without one
- least_busy — the eligible operator holding the fewest
Eligible means available, heartbeat fresh, covers this chatbot, and under maxConcurrent — all four. If nobody qualifies the conversation stays in the queue and the customer is told honestly; Bolcho will never invent an assignee to make a queue look tidy.
3 — Set the clocks
handoffSlaSec is how long a customer may wait for their first human reply before the conversation is flagged breached and raised in the queue. The clock stops on the first delivered reply, not on assignment — a claimed-then-ignored conversation is exactly the failure this measures. handoffAbandonSec marks a conversation abandoned after that much customer silence while still waiting; null means never.
Office hours
handoffHours on the assistant decides what an escalating customer is told. The handoff is never blocked — someone asking for a person has already decided the bot cannot help, and refusing to escalate strands them twice. What changes is the message and the expectation.
{
"handoffRouting": "round_robin",
"handoffSlaSec": 300,
"handoffAbandonSec": 1800,
"handoffAwayMessage": "We're closed until 9am — leave a message and we'll reply first thing.",
"handoffBusyMessage": "Everyone is with another customer. We'll be with you shortly.",
"handoffHours": {
"timezone": "Asia/Kolkata",
"days": { "mon": [{ "from": "09:00", "to": "18:00" }] },
"holidays": ["2026-12-25"]
}
}Acting as one of your operators
Your app authenticates with ONE workspace API key but has many agents behind it. Send x-voxa-actor: <externalId> on every inbox action so Bolcho knows who is claiming, replying, transferring and resolving — otherwise every action is anonymous and ?mine=true returns everyone's work. Add x-voxa-actor-name to make the audit timeline readable. Both headers are ignored for dashboard (JWT) callers, so a logged-in user cannot act as somebody else.
GET /public/chatbot/conversations/{id}/messages) returns a handoff object: either {state:"assigned", agentName, agentAvatarUrl} so the widget can say "Priya joined", or {state:"waiting", position, estimatedWaitSec}. The estimate is the median time-to-first-reply over recent conversations and is null when there is no history — an invented wait reads as a promise./inbox/operators/sync agents:writeSync the operator roster
Replace the workspace's operators. Anyone absent is marked unavailable rather than deleted.
Body
| operators* | object[] | The full roster. |
| operators[].externalId* | string | This person's id in YOUR system. Stable — it is what assignment is recorded against. |
| operators[].name* | string | Internal name, shown to your own staff. |
| operators[].displayName | string | Shown to the CUSTOMER when they join a chat. Falls back to `name`. |
| operators[].avatarUrl | string | Shown to the customer alongside displayName. |
| operators[].email | string | Informational. |
| operators[].available | boolean | Are they taking conversations right now? |
| operators[].maxConcurrent | number | Live conversations they may hold at once. Default 5. |
| operators[].agentIds | string[] | Chatbots they cover. Empty or omitted = all of them. |
| operators[].skills | string[] | Free-form capabilities, for skills-based routing later. |
curl -X POST https://api.bolchoai.in/v1/inbox/operators/sync -H "Authorization: Bearer $BOLCHO_API_KEY" -H "Content-Type: application/json" \
-d '{"operators":[{"externalId":"u_123","name":"Priya Sharma","displayName":"Priya","available":true,"maxConcurrent":3}]}'Response
{
"data": [
{ "externalId": "u_123", "name": "Priya Sharma", "displayName": "Priya", "available": true, "online": true, "openConversations": 2, "maxConcurrent": 3, "agentIds": [], "skills": [] }
],
"total": 1
}/inbox/operators/heartbeat agents:writeHeartbeat / set availability
Called on a timer (~30s) while an operator has their inbox open, and whenever they flip Available/Away. An unknown externalId is auto-registered rather than rejected.
Body
| externalId* | string | Who is checking in. |
| available | boolean | Omit to send a pure heartbeat without changing their status. |
curl -X POST https://api.bolchoai.in/v1/inbox/operators/heartbeat -H "Authorization: Bearer $BOLCHO_API_KEY" -H "Content-Type: application/json" \
-d '{"externalId":"u_123","available":true}'Response
{ "online": 3 }/inbox/operators agents:readList operators
The roster with who is genuinely online and how loaded each person is. `available` is what they said; `online` is whether their heartbeat is still fresh.
curl https://api.bolchoai.in/v1/inbox/operators -H "Authorization: Bearer $BOLCHO_API_KEY"/inbox/operators/{externalId} agents:writeRemove an operator
Hard-delete. Prefer omitting them from a sync, which keeps the audit trail intact.
Path parameters
| externalId* | string | Their id in your system. |
curl -X DELETE https://api.bolchoai.in/v1/inbox/operators/$EXTERNALID \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true }/inbox agents:readList conversations
Conversations needing a person, newest activity first, with a last-message preview.
Query parameters
| mine | boolean | Only the caller's own — resolved from `x-voxa-actor`. |
| status | string | `handoff`, `open`, `resolved` or `abandoned`. |
curl https://api.bolchoai.in/v1/inbox -H "Authorization: Bearer $BOLCHO_API_KEY" -H "x-voxa-actor: u_123"/inbox/{id}/claim agents:writeClaim a conversation
Take it. Race-safe: two operators claiming together means one gets a 409 naming who won, rather than both silently believing they have it.
Path parameters
| id* | uuid | Conversation id. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/claim \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true, "assignedTo": "u_123" }/inbox/{id}/release agents:writeRelease back to the queue
Only the operator holding it may release it.
Path parameters
| id* | uuid | Conversation id. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/release \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true, "assignedTo": null }/inbox/{id}/transfer agents:writeTransfer to a colleague
Hand it to a named operator, keeping the customer with a person. Distinct from release, which drops them back into an anonymous queue and loses who was dealing with it.
Path parameters
| id* | uuid | Conversation id. |
Body
| toExternalId* | string | The operator to hand it to. |
curl -X POST https://api.bolchoai.in/v1/inbox/c_123/transfer -H "Authorization: Bearer $BOLCHO_API_KEY" -H "Content-Type: application/json" \
-H "x-voxa-actor: u_123" -d '{"toExternalId":"u_456"}'Response
{ "ok": true, "assignedTo": "u_456", "assignedToName": "Rahul Verma" }/inbox/{id}/reply agents:writeReply to the customer
Delivered to the customer's channel first, then persisted — so a transcript never claims you said something they did not receive. On WhatsApp, outside Meta's 24-hour window the reply is stored and reported as undelivered with the reason.
Path parameters
| id* | uuid | Conversation id. |
Body
| content* | string | What to say. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/reply \
-H "Authorization: Bearer $BOLCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "string"
}'Response
{ "id": "m_1", "role": "assistant", "content": "…", "delivered": true, "deliveryFailedReason": null }/inbox/{id}/resolve agents:writeResolve
Mark it done. Only the operator holding it may resolve it.
Path parameters
| id* | uuid | Conversation id. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/resolve \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true }/inbox/{id}/reopen agents:writeHand back to the bot
Return the conversation to the assistant and clear the assignment.
Path parameters
| id* | uuid | Conversation id. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/reopen \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true }/inbox/{id}/notes agents:writeAdd a private note
Never reaches the customer and never enters the transcript — notes are events, not messages, so they cannot leak into an export, an analysis, or a WhatsApp send.
Path parameters
| id* | uuid | Conversation id. |
Body
| body* | string | The note. |
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/notes \
-H "Authorization: Bearer $BOLCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"body": "string"
}'Response
{ "ok": true }/inbox/{id}/timeline agents:readConversation timeline
Everything that happened that was not a message: assignment, transfers, releases, notes, tags, SLA breaches, abandonment — in order, with who did it.
Path parameters
| id* | uuid | Conversation id. |
curl https://api.bolchoai.in/v1/inbox/c_123/timeline -H "Authorization: Bearer $BOLCHO_API_KEY"Response
{
"data": [
{ "type": "auto_assigned", "actorName": "Priya Sharma", "body": "Routed to Priya Sharma", "createdAt": "2026-08-04T09:12:44.000Z" },
{ "type": "transferred", "actorName": "Priya Sharma", "body": "Transferred to Rahul Verma", "data": { "from": "u_123", "to": "u_456" } },
{ "type": "note", "actorName": "Priya Sharma", "body": "Customer already called twice about this." },
{ "type": "sla_breached", "actorName": "System", "body": "No human reply after 412s (target 300s)", "data": { "waitedSec": 412, "targetSec": 300 } }
],
"total": 4
}/inbox/canned-responses agents:readList saved replies
Workspace-wide entries plus any scoped to this chatbot.
Query parameters
| agentId | uuid | Narrow to one chatbot's replies plus the shared ones. |
curl https://api.bolchoai.in/v1/inbox/canned-responses \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "data": [ { "id": "cr_1", "shortcut": "hours", "title": "Opening hours", "body": "We're open 9-6, Monday to Saturday." } ], "total": 1 }/inbox/canned-responses agents:writeSave a reply
Upserts on `shortcut`, which is unique per workspace — saving under a name already in use replaces it rather than erroring.
Body
| shortcut* | string | Typed after "/" in the composer. Lower-cased; a leading slash is stripped. |
| title* | string | Shown in the picker. |
| body* | string | The text inserted. |
| agentId | uuid | Scope to one chatbot. Omit for workspace-wide. |
| sortOrder | number | Order in the picker. |
curl -X POST https://api.bolchoai.in/v1/inbox/canned-responses \
-H "Authorization: Bearer $BOLCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shortcut": "string",
"title": "string",
"body": "string",
"agentId": "00000000-0000-0000-0000-000000000000",
"sortOrder": 123
}'/inbox/canned-responses/{id} agents:writeDelete a saved reply
Remove it.
Path parameters
| id* | uuid | Saved reply id. |
curl -X DELETE https://api.bolchoai.in/v1/inbox/canned-responses/$ID \
-H "Authorization: Bearer $BOLCHO_API_KEY"Response
{ "ok": true }/inbox/presence/{agentId} agents:writeReport presence (legacy)
Reports a COUNT of available operators for one assistant. Superseded by the operator roster, which knows WHO is available and can therefore route; kept working for integrations that only ever pushed a number. If you sync operators, this is maintained for you.
Path parameters
| agentId* | uuid | Assistant id. |
Body
| available* | number | How many operators are at their desk. 0 = nobody. |
curl -X POST https://api.bolchoai.in/v1/inbox/presence/$AGENTID \
-H "Authorization: Bearer $BOLCHO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"available": 123
}'Response
{ "ok": true }