Skip to main content
A conversation walks a small state machine. Knowing the states — and how transitions emit webhooks and system messages — is enough to keep an external inbox or CRM in sync without polling.

States

StatusMeaning
bot_activeThe bot is currently handling the conversation. Visitor turns hit the model.
agent_requestedBot has handed off; conversation is sitting in the inbox waiting for a human reply.
openA human (or API caller) is actively working the thread. Bot is paused.
resolvedCustomer’s issue has been handled. End-of-conversation summary kicks off.
closedNo further activity expected. Use when the thread is done but not necessarily solved.
archivedHidden from the default inbox. Reversible via PATCH back to closed.
Pick resolved when you’ve answered the customer’s question; pick closed when the customer ghosted or the thread fizzled out. Both terminate the conversation, but resolved is the metric your CSAT report cares about.

Transition diagram

API cannot set bot_active. Once a human or API caller is in the loop, you cannot PATCH a conversation back to bot_active. Only the system places conversations into bot_active (on creation). The PATCH-allowed status enum is open | agent_requested | resolved | closed | archived.

Transitions

Trigger. The bot’s tool calls or the visitor explicitly asking (“speak to someone”). This transition is system-driven; it is also reachable from the API via PATCH status=agent_requested if you want to escalate based on your own logic.Side effects.
  • Inserts a system marker message visible to the visitor.
  • Emits a status_change row on conversation_events.
  • Fires conversation.updated.
Trigger. The first POST /conversations/{id}/messages (API) or the dashboard’s Take over button. Cannot be reached by PATCH directly — sending a reply is what claims the conversation.Side effects.
  • Inserts a “A team member has joined the conversation.” system marker.
  • Emits a human_takeover event row.
  • Fires message.created (marker), conversation.updated (with {from: "agent_requested", to: "open"}), then message.created (the agent reply).
Trigger. PATCH /conversations/{id} with {"status": "resolved"} or the dashboard’s Resolve button.Side effects.
  • Stamps resolved_at.
  • Inserts a “This conversation has been resolved.” system marker.
  • Emits a status_change event row.
  • Fires message.created (marker), conversation.updated.
  • Schedules the end-of-conversation summary job that stamps summary and sentiment_score.
Trigger. PATCH /conversations/{id} with {"status": "closed"}. Use when the thread is done but not necessarily solved (visitor ghosted, etc).Side effects.
  • Stamps closed_at.
  • Inserts a “This conversation has been closed.” system marker.
  • Emits a status_change event row.
  • Fires message.created (marker), conversation.updated.
  • Schedules the end-of-conversation summary job.
Trigger. Inactivity timer (default 7 days after resolved_at). System-driven; no equivalent API call.Side effects.
  • Stamps closed_at.
  • No system marker — the resolved state already announced the end of the thread.
  • Fires conversation.updated.
Trigger. PATCH /conversations/{id} with {"status": "archived"}. Hides the conversation from the default inbox.Side effects.
  • Stamps archived_at.
  • No system marker (hidden from visitor view anyway).
  • Fires conversation.updated.
Trigger. PATCH /conversations/{id} with {"status": "open"}. The dashboard requires contact_email (to email a resume link), but the API does not — you can reopen any conversation programmatically.Side effects.
  • Clears resolved_at / closed_at.
  • Emits a status_change event row.
  • Fires conversation.updated.

Webhook events per transition

TransitionEvents
any → agent_requestedconversation.updated
agent_requestedopen (via agent reply)message.created (takeover marker), conversation.updated, message.created (the reply)
openresolvedmessage.created (marker), conversation.updated
openclosedmessage.created (marker), conversation.updated
resolvedclosed (auto-closure)conversation.updated
any → archivedconversation.updated
archivedclosed (unarchive)conversation.updated
Every PATCH /conversations/{id} that changes any field — not just status — fires conversation.updated. The webhook payload includes a changes object with {from, to} for every modified field.

System marker messages

Several transitions insert a system-role message into the thread so the visitor sees what happened. These show up in GET /conversations/{id}/messages with role: "system" and metadata.event set to one of:
Event markerInserted text
human_takeover”A team member has joined the conversation.”
status_changeresolved”This conversation has been resolved.”
status_changeclosed”This conversation has been closed.”
status_changearchived(hidden from the visitor)
Filter these out client-side if your UI surfaces them differently than visitor/agent messages.