01
How it works#
Your agent reports on its own conversations through MCP or the API: the sentiment of each message it reads or sends, how each conversation ended, and when a conversation needs a person. BotInbox stores the labels, shows them in the dashboard under Conversations, and acts on handoffs: it notifies your team and holds the agent's replies until a person resolves the handoff. People can override any label from the conversation view. Any full-access key can label the threads its scope reaches (account, pod or inbox); sending-only keys get 403 insufficient_scope.
02
Message sentiment#
Set it on inbound messages (the customer's tone) and outbound messages (the tone of the agent's reply). sentiment is positive, neutral or negative; score is optional, from -1 to 1, and must agree with the sentiment; note is optional, up to 500 characters. Setting it again replaces the previous value.
curl https://botinbox.dev/v0/inboxes/INBOX_ID/messages/MESSAGE_ID/sentiment \
-X PUT -H "Authorization: Bearer $BOTINBOX_API_KEY" -H "Content-Type: application/json" \
-d '{"sentiment":"negative","score":-0.7,"note":"Package arrived damaged"}'03
Conversation outcome#
outcome is open, resolved, unresolved or escalated. success defaults to true for resolved and false for unresolved and escalated; pass it to say otherwise. reason (up to 500 characters) and confidence (0 to 1) are optional. The success ratio in the dashboard is successful conversations divided by closed ones (resolved, unresolved or escalated). Each change emits the thread.outcome_set webhook.
curl https://botinbox.dev/v0/inboxes/INBOX_ID/threads/THREAD_ID/outcome \
-X PUT -H "Authorization: Bearer $BOTINBOX_API_KEY" -H "Content-Type: application/json" \
-d '{"outcome":"resolved","reason":"Sent a replacement","confidence":0.9}'04
Request a human handoff#
Ask for a person when the customer asks for one, is angry or frustrated, the topic is billing, refunds, security, legal or account access, or the agent is unsure. reason is required (up to 500 characters, written for your teammate); priority is low, normal (default), high or urgent; category is customer_request, frustrated, billing, security, legal, uncertain or other (default). Owners and Admins get an email and the thread.handoff_requested webhook fires. Asking again while a handoff is open returns it with already_open: true and notifies nobody. An Idempotency-Key header is accepted.
curl https://botinbox.dev/v0/inboxes/INBOX_ID/threads/THREAD_ID/handoff \
-X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" -H "Content-Type: application/json" \
-d '{"reason":"Refund of $480 is above my approval limit","priority":"high","category":"billing"}'05
The reply hold#
While a handoff is open, agent replies on that conversation are refused with 409 thread_in_handoff: replies and draft sends through the API, MCP and the CLI. New conversations are never held, and people can always reply from the dashboard. The hold ends when the handoff is resolved or cancelled. It is on by default; an Owner or Admin can turn it off in the organization settings ("Hold agent replies during a human handoff"), which applies to handoffs requested afterwards.
{
"error": "This conversation was handed off to a person, so agent replies on it are on hold. ...",
"code": "thread_in_handoff",
"thread_id": "THREAD_ID",
"handoff_status": "requested"
}06
Cancel, claim and resolve#
The agent can cancel its own request with DELETE .../handoff until a person claims it (then 409 handoff_claimed). Claiming and resolving is done by a person in the dashboard: Conversations lists open handoffs by priority and age, with Claim and Resolve. Resolving can record the outcome at the same time and emits thread.handoff_resolved. Teams that run their own helpdesk can claim and resolve with an account-scoped full-access key; inbox and pod keys, the ones agents usually hold, can't, so an agent can never close its own handoff.
curl https://botinbox.dev/v0/inboxes/INBOX_ID/threads/THREAD_ID/handoff -X DELETE -H "Authorization: Bearer $BOTINBOX_API_KEY"
curl https://botinbox.dev/v0/inboxes/INBOX_ID/threads/THREAD_ID/handoff/claim -X POST -H "Authorization: Bearer $ACCOUNT_KEY"
curl https://botinbox.dev/v0/inboxes/INBOX_ID/threads/THREAD_ID/handoff/resolve -X POST \
-H "Authorization: Bearer $ACCOUNT_KEY" -H "Content-Type: application/json" \
-d '{"outcome":{"outcome":"resolved"}}'07
Read the insights#
GET .../threads/THREAD_ID/insights returns the outcome, the handoff (with agent_replies_held), each message's sentiment and the change history: who changed what and when, as an API key id or a user id. GET /v0/insights aggregates over the inboxes the key reaches: sentiment per day for inbound and outbound mail, outcomes and the success ratio, the handoff rate, and the median time to claim and to resolve (in seconds). Filter with from and to (YYYY-MM-DD or ISO 8601; the default is the last 14 days, at most 92), inbox_id and pod_id. Ratios and medians are null when there is nothing to measure.
curl "https://botinbox.dev/v0/insights?from=2026-09-01&to=2026-09-28&inbox_id=INBOX_ID" -H "Authorization: Bearer $BOTINBOX_API_KEY"08
MCP tools#
set_message_sentiment, set_conversation_outcome, request_human_handoff, cancel_human_handoff and get_conversation_insights (a thread's insights plus a summary for the inbox). The tool descriptions tell the agent when to use each one. get_thread also returns handoff_status and agent_replies_held.
{"name":"request_human_handoff","arguments":{"thread_id":"THREAD_ID","reason":"Customer asked for a manager","priority":"high","category":"customer_request"}}