Human takeover
Sometimes a conversation should move from the agent to a person: a sensitive case, an angry customer, a question the knowledge base cannot answer. Human takeover lets an operator own the conversation while the AI stays quiet, then hand it back when the person is done.
Ownership states
Each conversation is either ai_owned or human_owned.
When a conversation is ai_owned, visitor messages follow the normal assistant path — routines, retrieval, skills, model calls, and outbox actions all run.
When a conversation is human_owned, Radioso suppresses the AI turn. New visitor messages are still saved, but the assistant does not run routines, retrieve, dispatch skills, call the answer model, or enqueue outbox actions.
Before a teammate replies, the assistant — the chat surface your agent speaks through — returns one short waiting line instead of an answer — “a teammate is joining, please wait” — generated by the model in the conversation’s language, so it is multilingual. Once an operator has replied in the thread, the teammate has joined: later visitor messages get no AI reply and wait, so the visitor is never told a teammate is “joining” when one is already there. The AI stays silent until an operator hands the conversation back.
How a handoff is requested
Handoff is request-driven. The AI does not silently decide to transfer a conversation. There are three triggers:
- A routine reaches a
handoffterminal — including a branch an LLM-selected transition chooses, such as an authored path for an annoyed user. The reason isroutine_handoff. - A routine with a
handoffterminal gets a visitor stuck: a step’s question is re-asked three times in a row, asked once more in different words, and the visitor’s reply still gives it nothing. The routine ends on that step without running its terminal or completion export, and the reason isroutine_stuck. See Author a routine. - An agent has
handoffOnRetrievalMissenabled and a turn produces a no-context grounded miss. This is opt-in per agent and off by default. The reason isretrieval_miss.
In the agent’s Profile → Answers settings, turn on Hand off on retrieval miss to enable this trigger.
Every trigger requests human ownership, notifies an operator through the contact-delivery transport with a handoff.notify action, and records a hitl.ownership audit event carrying the reason. A routine hand-off’s notice, whether from an authored ending or a stuck routine, defaults its subject to the routine’s name and lists every value the routine collected; a retrieval-miss hand-off has no routine to name and nothing collected, so its notice uses the generic default subject and lists no values. Both carry the headline, any authored intro, the entry page, and a link to the conversation in the email and Slack text. The webhook JSON carries the ids, the reason, and the routine — its id and name for a routine hand-off, null for a retrieval miss. The webhook body is documented under Operator notifications.
A routine can also notify operators without handing off: a finish ending with Notify the team on sends the same kind of notice through a completion.notify action, with reason set to routine_completed. The conversation stays with the agent, so that notice changes no ownership and creates no Inbox item.
An authored hand-off or finish ending can send its notice through a specific notify skill instead of the default contact chain — the Send with choice in the routine editor’s ending panel, described in Author a routine. The email and webhook then go to that skill’s own recipients; the Slack post is unaffected either way. A stuck-routine or retrieval-miss hand-off has no authored ending to carry that choice, so both always use the default chain. To see where a given ending’s notice actually goes, open its Send with line in the editor, or call GET /api/v1/agents/{agentId}/operator-notice-destinations, documented in Agents and skills.
Operator console
The dashboard surfaces this work in the Inbox, a top-level sidebar item ahead of Agents, Knowledge Base, Audience Pulse, and Quality, with a badge showing how many items are open.
The Inbox opens with a lens toggle at the top of its left pane. Needs you is the default: the queue of everything waiting on a person — handoffs, approvals, and negative feedback. All lists every conversation, newest first, each row titled with a short topic the model generates from the conversation — falling back to the visitor’s opening message until a topic exists — with an outcome chip (In progress, Completed, or Handed off) plus search and filters for outcome, agent, and site.
In Needs you, the left pane lists open items with search and filters for type, agent, channel, and Taken by, which groups items by teammate — Me matches the conversations you hold. Type, agent, and channel stay in the URL, so you can share a narrowed queue or return to it after a refresh; they combine, so Handoffs from the email channel is one view. Critical escalations — an approval to decide, a handoff awaiting or held by a human — sort to the top, then written thumbs-down feedback, ordered by its latest creation or edit. Automatically detected signals and uncommented feedback stay in Quality instead of adding one inbox row per answer.
A handoff row names who holds it, so two teammates do not pick up the same conversation. One you hold reads “You are handling this” in the accent colour; one a teammate holds reads “Dana Scully is handling this” on a muted, dashed row. A handoff nobody holds names no one.
Feedback rows carry a checkbox, and only feedback rows do. Tick several rows, or tick one and choose Select all to take every visible feedback row, then choose Dismiss and pick why: Expected behavior, Outside the agent’s scope, Invalid or insufficient feedback, Other with a short note, or Close without reason. The reason applies to every selected row. Each row is dismissed with the same PUT /api/v1/quality/turns/{assistantMessageId}/triage request a single dismissal makes, so each writes the same triage history entry and the same “dismissed the feedback” activity line, and each needs the Quality manage permission. The queue and the counts update as each row lands. A row that fails stays in the queue, still selected, and is listed under Not dismissed with its own reason — “Could not dismiss. Try again.”, “Another operator changed it. Try again.”, or “You do not have permission to dismiss feedback.” A row a teammate closed first leaves the queue and is listed as “Another operator already closed it.” Dismissing again retries the rows that stayed. A row a filter hides is never dismissed with the selection.
Both lenses share the same reading pane. Select an actionable conversation — one waiting on a human or already human-owned — and it opens: a one-line header naming the visitor, the page they were on, and how long they’ve been waiting; a situation card with the handoff reason and the visitor’s opening request; the live transcript; and a reply composer. Send a reply to a conversation nobody holds yet and you’ve claimed it — there’s no separate take-over step first. Assign next to Send places a conversation waiting for a teammate: Me first, then every teammate who can take it. Once someone holds the conversation, the menu reads Reassign and lists Me and every teammate except whoever holds it; on one you hold, it lists your teammates. Choose a teammate and the conversation is theirs, with an email linking to it when email delivery is configured. When a teammate already holds the conversation, the composer gives way to “Dana Scully is handling this · Reassign”, so two people never reply blind; Reassign → Me transfers it to you and brings the composer back, with anything you’d drafted still in it. If the teammate you chose can no longer take the conversation — they were disabled or left the workspace since the list loaded — the pane says “That teammate can no longer take this.” (or “You can no longer take this.” for Me), keeps your draft, and reloads the teammate list. A human reply’s badge names the teammate who wrote it, by display name or email, even when the visitor saw it from “A teammate”. Between the messages, short muted lines tell you what happened and who did it: “Handed off · No answer found”, “Dana Scully took this”, “Dana Scully assigned this to Fox”, “Fox handed this back to the agent”, “Dana Scully decided · Approve refund”, “Fox resolved the feedback”. While the item is open, new visitor messages, your own replies, and a take-over, reassignment, or hand-back made elsewhere appear, line included, without a manual refresh. Until the dashboard knows who you are, the pane shows none of the controls that depend on it — no “is handling this” line, no Assign or Reassign, and no Done on a handoff. Choose Done to close a handoff and hand the conversation back to the agent; it shows when you hold the conversation or nobody has claimed it. A conversation shows as one row: while it also has an open handoff or approval, the Inbox folds its negative feedback into that row instead of listing it separately, so Done there closes the handoff or approval, not the feedback. Once the conversation is back with the agent, the feedback appears as its own item, and Done on it opens the same resolution-reason flow Quality → Review uses to classify it — closing it leaves ownership alone. An approval closes when you choose one of its decision options — it needs no separate Done step. Select any other conversation and the reading pane is read-only, with an outcome footer in place of the composer.
Under the queue, Recently closed keeps the last ten items you and your teammates closed — handoffs handed back, approvals decided, negative feedback resolved or dismissed — each with what it was and who closed it: “Handoff · Closed by Dana Scully · 30 Sept, 2:12 PM”. Select one to open its conversation in the reading pane, activity lines and all. Resolved and dismissed feedback, there and in the activity lines, shows only to teammates with Quality access — owners and admins.
The browser tab title shows how many items are open, and a soft sound plays when a new handoff or approval arrives while you have the dashboard open.
Either lens has an Open in debug view link, which opens the conversation drawer: transcript with the same activity lines, Debug, Flow, a button to continue it in test chat, and a button to send it to Eval. Owners and admins also get Delete conversation, which erases it permanently — see Deleting conversation data. The drawer is for inspecting and testing — replying, taking over, handing back, and approving or rejecting a pending decision all happen from the reading pane instead.
A voice call started from the website widget shows Listen in the header, next to Open in debug view, once the widget has told Radioso which ElevenLabs conversation the call was. Listen plays the call in the pane, streamed from your ElevenLabs account through Radioso with the workspace’s key. Nothing is fetched until you press it, a conversation you come back to starts silent, and every play is recorded in the audit log under your name. ElevenLabs finishes a recording shortly after the call ends, so until then the player reads “The recording can’t be played right now.”
You can answer a caller from the reading pane too. A call speaks only when its caller does, so your reply waits for the caller’s next utterance and is then spoken in place of the agent’s waiting line; the line beside Send says so: “The caller hears this on their next turn.” The reply shows Pending until the call speaks it, then Spoken; a reply with nothing a call can say, such as only formatting marks, shows Not spoken: no words to say. Several replies written between two utterances are spoken in order on the same turn, and a reply still pending when you hand the call back is spoken before the agent answers. A reply written after the caller hangs up stays Pending. History reads carry the same state as the reply’s speech field — see Chat and history.
The recording stays in your ElevenLabs account, in its data residency region, until it is deleted there or Radioso deletes it. Deleting the conversation, its visitor, the workspace or the organization asks ElevenLabs to delete the call’s recording (Deleting conversation data). Deleting an agent doesn’t: its conversations stay, and their recordings still play. Radioso deletes recordings with the workspace’s ElevenLabs key. If you remove that key, or replace it with a key for another ElevenLabs account, the old account’s recordings can no longer be played or deleted from Radioso; delete them in ElevenLabs. A deletion never waits on ElevenLabs: a background job deletes the recordings afterwards, with a copy of the key the deletion kept, and asks again for about a day when ElevenLabs refuses. Owners and admins find any recording still there after that in GET /api/v1/account/voice-recordings/left-at-vendor, with its ElevenLabs conversation id, so you can delete it in ElevenLabs (Deleting conversation data).
The drawer opens with a Visitor panel above the transcript whenever the conversation carries a visitor or a request context: country, region, and city; browser and OS, with the raw user agent in a tooltip; language; entry page; referrer; IP address; and when the visitor first showed up. An “unverified” hint means an edge marker arrived with the request but did not check out, so no facts are shown for it. Below those fields, Previous conversations lists the visitor’s five most recent other conversations with a link to each and a “see all N” count when there are more — this is what groups conversations under the same durable visitor key from the embed, or the same verified customer id, across sessions. A field with nothing to show renders as unknown rather than being hidden, so a thin profile still confirms the panel worked.
Conversation rows across the Inbox and Quality lists show the country code next to the visitor label when a geo header resolved one, ahead of the page or channel it names.
Quality is its own top-level section with two pages. Review is the paginated answer-quality backlog and per-turn triage: negative feedback and skill failures, alongside the grounding gaps summarized in the Inbox. The Unmet directives chip lists answers that graded one of their own directives as not followed; it’s opt-in, so those answers stay out of the default queue and the Inbox count until you choose it. Retrieval answers with a complete diagnostic show the sourced claim total and separate warnings for unsourced claims or invalid citations under the Outcome badge. A no-support answer with zero claims says No supported claims; turns without complete evidence show no diagnostic line. Open Filter → Evidence to select grounding verdicts or focus on unsourced claims and invalid sources. Closed work can be filtered by structured resolution reason and closure time, and the compact resolution breakdown opens the exact matching queue. Filter state stays in the URL, so a review queue can be shared. Add to Eval preserves a weak turn as a repeatable case on the Evals page; once linked, the row shows timestamped verification evidence and the action becomes Open Eval.
GET /api/v1/quality/turns exposes the same detail as a complete grounding object or null. The groundingVerdict, hasUnsourcedClaims, and hasInvalidSources query parameters filter it server-side. A false count-presence filter includes complete zero-count diagnostics and excludes unknown diagnostics.
A turn counts as a grounding gap only when the agent tried to ground an answer and found nothing. When it declines because the question sits outside what its instructions cover — the capital of Mars, a maths puzzle, an attempt to talk it out of its own remit — the turn carries the Out of scope action and sits on neither side of the grounded-answer rate. That leaves the gap queue holding the questions actually worth ingesting content for.
Operator API
You can act from the console or directly through authenticated endpoints under /api/v1/conversations. All of them require a workspace session with the workspace.conversation.takeover permission, and every action records a hitl.ownership audit event naming the acting teammate as actorUserId — and, on a transfer, the receiving teammate as targetUserId.
Ownership belongs to a person. The ownership record names the teammate handling the conversation in ownerUserId and labels them in ownerDisplayName: their display name, or their email until they set one. The label is read from their profile each time, so a rename shows in the Inbox at once. ownerAccountId is the workspace’s organisation, the same for every teammate. The same ownership, label included, reaches anyone who reads the workspace’s history with an API token or through the operator MCP server, so a teammate who hasn’t set a display name is visible to them by email.
A human-owned conversation is claimed exactly when it names a teammate in ownerUserId. One with ownerUserId: null waits for a teammate: the AI stays out of it, and the next teammate to take it over, reply, or receive a transfer claims it. A record with ownerUserId: null also has ownerDisplayName: null and takenOverAt: null. When a teammate’s user is deleted, the conversations they held wait for a teammate this way, and nothing names the deleted teammate.
Every action records a hitl.ownership audit event naming the acting teammate as actorUserId, once the action has committed. The action stands even if that write fails: the endpoint still answers with the new ownership, and the failure is logged and reported with the conversation and teammate ids. The conversation’s own record of the action is its activity, which commits together with the action or not at all.
The server applies the rules below on every surface: the dashboard, this API, and Slack.
Take over
POST /api/v1/conversations/{conversationId}/takeover{ "reason": "operator_takeover" }reason is optional. The response returns the current ownership record, with you as ownerUserId. Take over claims a conversation that is AI-owned or waiting for a teammate; on one you already hold it returns the record unchanged. One a teammate holds returns 409 with the current ownership in error.details.ownership — take it from them with a transfer to yourself, which is what the dashboard’s Reassign → Me does on a teammate’s conversation.
Reply as a human
POST /api/v1/conversations/{conversationId}/reply{ "message": "Thanks for waiting. I can help with this.", "expectedVersion": 3 }The reply is saved as an assistant-role message with source: human_agent. Its metadata.humanAgent records who sent it — accountId and userId — and the signature the visitor sees, displayName: your display name, or your organisation’s name if you haven’t set one. It is never your email; with neither, the reply goes out unsigned and the visitor sees it from “A teammate”.
Every surface — the visitor chat and embed, the dashboard, Ray, and this API — carries a reply’s stored signature in operatorDisplayName by one rule: the signature shows as it was saved, unless it has an email address anywhere in it. That signature is never shown, whether or not the reply records its author in humanAgent.userId, and the visitor sees that reply from “A teammate”.
Operator reads also name the teammate who wrote each reply. The history detail and tail (GET /api/v1/history/chat/{conversationId} and its /tail) and Ray’s transcript tools add operatorLabel to a human-agent reply: the teammate label of the user in humanAgent.userId — their display name, or their email until they set one — read from their profile each time, so it follows a rename. A reply that records no author, or whose author’s user is deleted, carries its signature there instead, by the rule above. The dashboard badges a reply with operatorLabel, so teammates see who actually answered. The public chat API never returns operatorLabel.
Only the teammate who owns the conversation replies. A reply to a conversation the AI owns, or one waiting for a teammate, claims it for you first. When another teammate holds it, the endpoint returns 409 with code conflict, the message “Another teammate is handling this conversation”, and the current ownership in error.details.ownership. expectedVersion must match the ownership record you replied from; a stale value also returns 409 with the current record.
The ownership check, the saved reply, and its delivery to a customer channel such as Slack commit together, with the conversation and its ownership record locked in between. A transfer or hand-back that commits first refuses the reply with 409, and no message is saved; one that arrives while the reply is being saved waits for it. The Slack post is queued on the action outbox in the same database transaction, keyed by the message, so the worker posts each reply once and retries a failed post. A reply that could not be saved or queued leaves nothing behind, so after a 5xx you can send it again and the visitor sees it once.
The visitor, the dashboard, and the customer channel hear of a reply only once it has committed, and from then on the reply stands: the endpoint answers 201 even when pushing it to the visitor’s open chat or recording its audit event fails. Those failures are logged and reported with the conversation and message ids.
The 201 response carries message, the saved reply, and ownership, the ownership after the reply. Its version moves on when the reply claimed the conversation, so use the returned record for your next call.
On a voice conversation the reply is spoken on the caller’s next turn. Until then it carries speech: { "state": "pending" } in history, and { "state": "spoken", "spokenAt": "…" } once a turn has spoken it, where spokenAt is when that turn completed, recorded as a voice.operator_reply_spoken audit event with the message’s id. A reply with no words to say is { "state": "skipped" }.
List teammates
GET /api/v1/conversations/operators{
"operators": [
{ "userId": "6f1c2d4e-8a90-4b7c-9d1e-2f3a4b5c6d7e", "label": "Dana Scully" },
{ "userId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "label": "fox@example.com" }
]
}The teammates a conversation can be handed to: active users of the workspace’s organisation who hold workspace.conversation.takeover on it, each labelled by display name, or by email until they set one. A disabled teammate is left off the list and cannot receive a transfer.
Transfer ownership
POST /api/v1/conversations/{conversationId}/transfer{ "toUserId": "6f1c2d4e-8a90-4b7c-9d1e-2f3a4b5c6d7e", "expectedVersion": 3 }Any teammate with the takeover permission can hand a human-owned conversation to a teammate — one waiting for a teammate, or one someone holds. Pass your own userId to take a conversation from the teammate holding it; the dashboard’s Assign and Reassign menus make this call. toUserId must be one of the teammates List teammates returns; anyone else, including a disabled teammate or a user in another organisation, returns 404 with code transfer_target_unavailable. A conversation outside the workspace returns 404 with code not_found. expectedVersion is an optimistic-concurrency token from the ownership record; a stale value returns 409 with the current record.
When you hand a conversation to someone else and email delivery is configured, they get an email with a link to it in the dashboard, naming who handed it over and the workspace, and carrying none of the transcript. The transfer and its notice commit together: the notice is queued in the same database transaction and sent by the document worker, so the transfer returns without waiting on the mail provider and a failed send is retried. The worker sends the notice only if the recipient still holds the conversation at the ownership version the transfer produced, so a notice overtaken by a later transfer is dropped, even when the conversation comes back to the same teammate. Taking a conversation yourself sends nothing.
Hand back to the AI
POST /api/v1/conversations/{conversationId}/handback{ "expectedVersion": 4 }The teammate who owns the conversation hands it back, or anyone while it waits for a teammate. Anyone else gets 409 with the current ownership in error.details.ownership. After hand-back, the next visitor message follows the normal assistant path again.
List recently closed items
GET /api/v1/conversations/recently-closed?limit=10{
"items": [
{
"id": "9b0e7c1a-3f2d-4e5b-8a6c-1d2e3f4a5b6c",
"conversationId": "2c4e6a8b-0d1f-4a3c-9e5b-7d9f1b3c5e7a",
"itemKind": "approval",
"outcome": "approval_decided",
"closedAt": "2026-09-30T14:12:05.000Z",
"closedBy": { "userId": "6f1c2d4e-8a90-4b7c-9d1e-2f3a4b5c6d7e", "label": "Dana Scully" },
"decision": { "optionId": "approve", "label": "Approve refund" },
"resolution": null,
"assistantMessageId": null,
"title": "Refund for order 1042",
"preview": "Hi, I was charged twice for order 1042"
}
]
}The Inbox items the workspace closed most recently, newest first — the Recently closed strip reads this. itemKind is handoff, approval, or negative_feedback; outcome is the activity kind that closed it. closedBy is the teammate who closed it, labelled by display name or email, and null when a caller that is no teammate closed it — an API token resolving feedback, say — or the teammate’s user has since been deleted. limit takes 1 to 50 and defaults to 10, and dashboard test chats are left out. Negative feedback is listed only to a caller with Quality access (workspace.quality.read); for a member, the limit fills with handoffs and approvals.
Conversation activity
Every conversation keeps a record of what happened to it and who did it: the agent handing it off, a teammate taking it, assigning or reassigning it, handing it back, deciding one of its approvals, and resolving or dismissing its negative feedback. Each event is written in the same database transaction as the change it records, so you never see an action without its event, or an event for an action that didn’t happen. Replies aren’t events — they’re messages already.
Feedback resolved or dismissed in the 30 days before the record began is in it too, rebuilt from the triage history, so Recently closed starts with that month’s closed feedback.
The operator history detail and tail (GET /api/v1/history/chat/{conversationId} and its /tail) carry the record as activity, oldest first. The detail carries the whole list; so does a tail, along with an activityCursor. Pass that back as the next tail’s activityCursor and the tail carries the events recorded in the five minutes before the tail that issued it, and since. That window repeats events you already hold, and it is what lets a late commit through: an event is dated when it is written but visible only once its change commits, and an approval commits together with the routine turn it resumes, which can finish after a newer event. Keep each event once by its id. The cursor is opaque — pass back the one the last tail returned; a tail answers 400 to one it did not issue.
feedback_resolved and feedback_dismissed are Quality triage outcomes, so they reach only a caller with Quality access (workspace.quality.read). A member follows and takes over conversations without it and reads every other kind.
{
"id": "5e7a9c1b-3d5f-4a7c-9b1d-3f5a7c9e1b3d",
"kind": "reassigned",
"createdAt": "2026-09-30T14:05:40.000Z",
"actor": { "userId": "6f1c2d4e-8a90-4b7c-9d1e-2f3a4b5c6d7e", "label": "Dana Scully" },
"subject": { "userId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d", "label": "fox@example.com" },
"from": null,
"handoffReason": null,
"decision": null,
"resolution": null,
"assistantMessageId": null
}kind | What happened | Fields it fills |
|---|---|---|
handoff_requested | The agent asked for a person | handoffReason (routine_handoff, retrieval_miss) |
claimed | A teammate took the conversation, directly or by replying | actor |
reassigned | A teammate handed it to someone, themselves included | actor, subject (the new owner), from (the previous owner; null for a handoff nobody had claimed) |
handed_back | A teammate handed it back to the agent | actor |
approval_decided | A teammate chose an approval option | actor, decision (the option’s id and its label as the routine author wrote it) |
feedback_resolved, feedback_dismissed | A teammate closed negative feedback | actor, resolution (the triage resolution code), assistantMessageId |
actor is null when the agent acted, which only happens on handoff_requested. On any other kind, null means a caller that is no teammate made the change, or the teammate’s user has since been deleted. Teammates are labelled as they are now, so a rename shows at once — and because a label can be an email, activity is operator-only: the public chat API never returns it. Events carry ids and codes, never message content. Ray’s conversation transcript carries the latest 40 events, so you can ask it who took a handoff or which option a teammate chose — and, with Quality access, who closed the feedback.
Approvals
A routine can pause at an approval gate before a step with side effects. When a turn reaches the gate, the routine suspends, the assistant replies that the step needs review, and Radioso records a pending decision. The conversation is not handed over — it waits for an operator to decide. See Author a routine for how to author the gate.
List pending approvals
GET /api/v1/decisionsReturns the workspace’s open approval decisions, newest first. Each carries what an operator needs to decide and submit a resolve: handle, conversationId, agentId, routineId, stepId, reason, options, contentHash, deadline, and createdAt. Requires the workspace.conversation.takeover permission.
Resolve an approval
POST /api/v1/agents/{agentId}/decisions/{handle}/resolve{ "optionId": "approve", "contentHash": "sha256:...", "payload": null }optionId must be one of the decision’s options. contentHash must match the value from the pending decision; a stale hash returns 409. The decision flip, the routine resume, and the resumed turn commit in one transaction, so a crash before commit leaves the decision pending and a retry resolves cleanly. Approving resumes the routine and lets the gated action run; rejecting takes the rejection branch. A gated side effect runs as an idempotent outbox action, never inline. The same transaction records who decided and which option they chose, as an approval_decided activity event.
Live updates
Both surfaces can read forward for new messages instead of refetching the whole transcript:
GET /api/v1/history/chat/{conversationId}/tail?cursor=...&activityCursor=... (operator)
GET /api/v1/public/chat/{token}/tail/{conversationId}?cursor=... (visitor)
GET /api/v1/public/chat/{token}/events/{conversationId} (visitor push, SSE)Each tail call returns messages created after the cursor plus an advanced cursor. With no cursor, tail returns the newest bounded page. Messages include source, so a visitor sees a human reply distinctly, plus operatorDisplayName on a human-agent reply so the visitor can see who is answering. That name is the reply’s signature — the teammate’s display name, or the organisation’s name — and never an email, user id, or account id; an unsigned reply, or one whose only signature is an email address, shows as “A teammate”. The operator tail also includes each reply’s operatorLabel and, once a teammate has been involved in the conversation, its ownership record — AI-owned after a hand-back — so a reader polling the tail sees a claim, transfer, or hand-back made elsewhere. The operator conversation detail carries the same record, so whichever of the two a reader loaded last, the higher version is the current one. Both also carry the conversation’s activity. The visitor tail and detail carry none of these.
Common failure modes
- A
409on reply, transfer, or handback means the ownership record moved since you read it. Re-read the current ownership fromerror.details.ownershipand retry with the newexpectedVersion. - A reply or handback returns
409with “Another teammate is handling this conversation”: a teammate holds it. Leave it to them, or transfer it to yourself first. - The AI keeps answering a conversation you meant to own: no one has taken it over yet. Ownership flips only on an explicit takeover, a routine handoff, or a retrieval-miss handoff.
- A transfer returns
404withtransfer_target_unavailable:toUserIdis not an active, enabled teammate who can own conversations in this workspace. Pick a target from List teammates. - A bulk dismissal lists a row under Not dismissed: “Could not dismiss. Try again.” is a request that failed, so choose Dismiss again; “Another operator changed it. Try again.” means a teammate acknowledged or reopened it since the queue loaded, and the row now carries their version; “You do not have permission to dismiss feedback.” means your role cannot manage Quality.
- A takeover returns
409although nobody replied yet: a teammate holds the conversation. Transfer it to yourself to take it from them. - A conversation someone handled shows as waiting for a teammate, with no owner named: it was claimed under organisation-level ownership, or its owner’s user was deleted. The next teammate to act on it claims it.
- Recently closed shows “Closed” with no name: a caller that is no teammate closed the item — usually an API token resolving feedback — or the teammate’s user was deleted.
- A teammate sees no resolved or dismissed feedback in Recently closed or the activity lines: they are a member, without Quality access. Owners and admins see it.
- The visitor never sees a “joining” line: the line is model-generated, and a conversation where an operator has already replied gets no waiting line at all — later messages just wait.
- A reply to a caller stays Pending: the caller hasn’t spoken since you sent it, or the call has ended. A call speaks only when its caller does, so the reply goes out with their next utterance.
Read next
- Author a routine — author the handoff terminals and approval gates that trigger takeover.
- Slack channel — approve, take over, and reply from Slack.
- Clarification behavior — how the agent asks before it escalates.
- Visitor data — every field the Visitor panel shows and how long it lives.