Bolcho

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.

Availability needs BOTH facts. An operator marked available whose heartbeat has expired is someone who ticked the box and shut their laptop — Bolcho will not route to them, because a conversation that looks handled and is not is worse than one still visibly in the queue.

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.

json
{
  "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.

The visitor's own poll (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.
POST/inbox/operators/sync agents:write

Sync the operator roster

Replace the workspace's operators. Anyone absent is marked unavailable rather than deleted.

Body

operators*object[]The full roster.
operators[].externalId*stringThis person's id in YOUR system. Stable — it is what assignment is recorded against.
operators[].name*stringInternal name, shown to your own staff.
operators[].displayNamestringShown to the CUSTOMER when they join a chat. Falls back to `name`.
operators[].avatarUrlstringShown to the customer alongside displayName.
operators[].emailstringInformational.
operators[].availablebooleanAre they taking conversations right now?
operators[].maxConcurrentnumberLive conversations they may hold at once. Default 5.
operators[].agentIdsstring[]Chatbots they cover. Empty or omitted = all of them.
operators[].skillsstring[]Free-form capabilities, for skills-based routing later.
bash
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

json
{
  "data": [
    { "externalId": "u_123", "name": "Priya Sharma", "displayName": "Priya", "available": true, "online": true, "openConversations": 2, "maxConcurrent": 3, "agentIds": [], "skills": [] }
  ],
  "total": 1
}
POST/inbox/operators/heartbeat agents:write

Heartbeat / 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*stringWho is checking in.
availablebooleanOmit to send a pure heartbeat without changing their status.
bash
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

json
{ "online": 3 }
GET/inbox/operators agents:read

List 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.

bash
curl https://api.bolchoai.in/v1/inbox/operators -H "Authorization: Bearer $BOLCHO_API_KEY"
DELETE/inbox/operators/{externalId} agents:write

Remove an operator

Hard-delete. Prefer omitting them from a sync, which keeps the audit trail intact.

Path parameters

externalId*stringTheir id in your system.
bash
curl -X DELETE https://api.bolchoai.in/v1/inbox/operators/$EXTERNALID \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true }
GET/inbox agents:read

List conversations

Conversations needing a person, newest activity first, with a last-message preview.

Query parameters

minebooleanOnly the caller's own — resolved from `x-voxa-actor`.
statusstring`handoff`, `open`, `resolved` or `abandoned`.
bash
curl https://api.bolchoai.in/v1/inbox -H "Authorization: Bearer $BOLCHO_API_KEY" -H "x-voxa-actor: u_123"
POST/inbox/{id}/claim agents:write

Claim 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*uuidConversation id.
bash
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/claim \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true, "assignedTo": "u_123" }
POST/inbox/{id}/release agents:write

Release back to the queue

Only the operator holding it may release it.

Path parameters

id*uuidConversation id.
bash
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/release \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true, "assignedTo": null }
POST/inbox/{id}/transfer agents:write

Transfer 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*uuidConversation id.

Body

toExternalId*stringThe operator to hand it to.
bash
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

json
{ "ok": true, "assignedTo": "u_456", "assignedToName": "Rahul Verma" }
POST/inbox/{id}/reply agents:write

Reply 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*uuidConversation id.

Body

content*stringWhat to say.
bash
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

json
{ "id": "m_1", "role": "assistant", "content": "…", "delivered": true, "deliveryFailedReason": null }
POST/inbox/{id}/resolve agents:write

Resolve

Mark it done. Only the operator holding it may resolve it.

Path parameters

id*uuidConversation id.
bash
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/resolve \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true }
POST/inbox/{id}/reopen agents:write

Hand back to the bot

Return the conversation to the assistant and clear the assignment.

Path parameters

id*uuidConversation id.
bash
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/reopen \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true }
POST/inbox/{id}/notes agents:write

Add 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*uuidConversation id.

Body

body*stringThe note.
bash
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

json
{ "ok": true }
POST/inbox/{id}/tags agents:write

Add a tag

Idempotent — adding the same tag twice is not an error.

Path parameters

id*uuidConversation id.

Body

tag*stringThe label.
bash
curl -X POST https://api.bolchoai.in/v1/inbox/$ID/tags \
  -H "Authorization: Bearer $BOLCHO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "tag": "string"
}'

Response

json
{ "ok": true, "tags": ["billing"] }
DELETE/inbox/{id}/tags/{tag} agents:write

Remove a tag

Remove a label.

Path parameters

id*uuidConversation id.
tag*stringThe label to remove.
bash
curl -X DELETE https://api.bolchoai.in/v1/inbox/$ID/tags/$TAG \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true, "tags": [] }
GET/inbox/{id}/timeline agents:read

Conversation 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*uuidConversation id.
bash
curl https://api.bolchoai.in/v1/inbox/c_123/timeline -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{
  "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
}
GET/inbox/canned-responses agents:read

List saved replies

Workspace-wide entries plus any scoped to this chatbot.

Query parameters

agentIduuidNarrow to one chatbot's replies plus the shared ones.
bash
curl https://api.bolchoai.in/v1/inbox/canned-responses \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "data": [ { "id": "cr_1", "shortcut": "hours", "title": "Opening hours", "body": "We're open 9-6, Monday to Saturday." } ], "total": 1 }
POST/inbox/canned-responses agents:write

Save a reply

Upserts on `shortcut`, which is unique per workspace — saving under a name already in use replaces it rather than erroring.

Body

shortcut*stringTyped after "/" in the composer. Lower-cased; a leading slash is stripped.
title*stringShown in the picker.
body*stringThe text inserted.
agentIduuidScope to one chatbot. Omit for workspace-wide.
sortOrdernumberOrder in the picker.
bash
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
}'
DELETE/inbox/canned-responses/{id} agents:write

Delete a saved reply

Remove it.

Path parameters

id*uuidSaved reply id.
bash
curl -X DELETE https://api.bolchoai.in/v1/inbox/canned-responses/$ID \
  -H "Authorization: Bearer $BOLCHO_API_KEY"

Response

json
{ "ok": true }
POST/inbox/presence/{agentId} agents:write

Report 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*uuidAssistant id.

Body

available*numberHow many operators are at their desk. 0 = nobody.
bash
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

json
{ "ok": true }
Bolcho — Voice AI for Bharat