Try the interactive chat
Human handoff and team replies
How conversations move to your team and how staff replies reach the visitor.
Every API conversation appears in Sonecentral's Mesajlar (Messages) screen with source API. The visitor is called API ziyaretçisi (API visitor), because their identity is unknown. Your team can read, take over and reply there.
How handoff works
As on WhatsApp, the assistant hands a conversation to the team when the visitor asks for a person, the assistant cannot answer, or business rules require it. It tells the visitor about the handoff.
A staff member writing or sharing a product, service or document in Messages also moves the conversation to the team. The assistant stops replying until the team returns it to the assistant.
The turn's status indicates the outcome:
status | Outcome | Client action |
|---|---|---|
answered | The assistant replied. | None. |
handoff | This turn moved the conversation to the team and sent a message, usually explaining the handoff. | Display the message and start polling replies. |
human | The team owns the conversation. The assistant did not reply; the message reached the team. | Start or continue polling replies. |
silent | No reply needed. | None. |
status appears in each format:
- Chat Completions:
choices[0].message.sone.statusfor non-streamed responses, or top-levelsone.statuson the final stream chunk. - Responses: top-level
sone.status. - Vercel AI SDK:
messageMetadata.statusonfinish.
Visitors can keep writing during handoff. Send messages through the usual endpoints; they return human and reach the team.
Always send the conversation ID. Team-owned conversations cannot be continued using history or useChat's id alone. A client retaining only role/content, or only the browser-generated ID, starts a separate conversation where the assistant replies. See Conversation lookup.
Keep sending the ID after ownership returns to the assistant. Without it, continuation requires the assistant's own generated latest reply, a verbatim history including it, and history starting at the first message. Stock phrases, greetings and staff replies are insufficient. Polling also needs the conversation ID. Add an Idempotency-Key to each message; a retry without message identity after staff write can otherwise duplicate it.
Read team replies
Staff replies do not appear in the original stream, since they can arrive later. Read conversation messages instead:
GET https://pb.sonecentral.com/api/v1/conversations/{id}/messages?after={last_message_id}curl "https://pb.sonecentral.com/api/v1/conversations/conv_x8k2m9p4q7w1r3t/messages?after=msg_p4q7w1x8k2m9a5z" \
-H "Authorization: Bearer $SONE_API_KEY"{
"object": "list",
"data": [
{
"id": "msg_v2k8r4t1m9q6w3e",
"role": "user",
"type": "text",
"text": "Would Thursday at 15:00 work?",
"created_at": 1760867520
},
{
"id": "msg_s7t4f2k9w1q3n8d",
"role": "staff",
"type": "text",
"text": "Hello, I am Elif. I noted your request for Thursday at 15:00 and will reply here once confirmed.",
"created_at": 1760867538
}
],
"has_more": false,
"mode": "team",
"reply_pending": false,
"first_id": "msg_v2k8r4t1m9q6w3e",
"last_id": "msg_s7t4f2k9w1q3n8d"
}Prop
Type
| Field | Meaning |
|---|---|
data | Oldest-first SoneMessage array. Staff can also send products, services, documents and buttons. |
has_more | More messages exist. Fetch immediately using last_id. |
mode | "team" or "assistant". Assistant ownership does not imply the reply has finished. |
reply_pending | The assistant has a running, queued or recoverable reply. Continue polling even with mode: "assistant". |
first_id, last_id | IDs of the first and last messages on this page. Omitted when data is empty. |
- All roles are included: visitor
user, assistantassistant, and teamstaff. Do not redraw visitor messages already shown. - Always advance
afterto responselast_id. Visitor messages also advance the cursor, preventing repeated reads. - An
afteroutside this conversation returns400 invalid_cursor. Clear the cursor and read from the beginning. - A non-API conversation, wrong workspace, or assistant unavailable to the key returns
404 conversation_not_found. Any key in the workspace allowed to call the same assistant can read it. - Every read counts toward limits. Publishable-key reads have a separate hourly allowance from message sending.
Staff shares
Staff can share inventory products, services and Knowledge Base documents from Messages, just as on WhatsApp. They use the same structure as assistant shares, with role: "staff":
| Shared item | type | Data |
|---|---|---|
| Product or service | catalog | One record per staff share in catalog.items; kind is inventory for products and offering for services. |
| Document | file | Signed document download URL in file.url. |
{
"id": "msg_k8r3t6w1q9m2x5c",
"role": "staff",
"type": "catalog",
"text": "Haircut\nIncludes wash and blow-dry.\n750₺\n45 minutes\nhttps://www.ornek.com/hizmet/sac-kesimi",
"created_at": 1760867590,
"catalog": {
"format": "card",
"items": [
{
"id": "o5n2k8w4q1t7m3x",
"kind": "offering",
"title": "Haircut",
"description": "Includes wash and blow-dry.",
"price": "750₺",
"detail": "45 minutes",
"url": "https://www.ornek.com/hizmet/sac-kesimi",
"button_title": "Hizmeti incele"
}
]
}
}- A share moves ownership to the team and appears in the message list, not the original stream. If staff share during an assistant conversation, the next visitor turn returns
human. Poll after the last message from the previous turn to retrieve the share. - If a share answers a visitor message and you retry it with the same
Idempotency-Key, the share also appears among that turn's messages. - Every share includes
textfor text-only clients. Long catalog fallback text may be shortened;catalog.itemsremains complete. - Render cards and download files as for assistant shares: Rich messages.
Conversation state
To check ownership without reading messages:
GET https://pb.sonecentral.com/api/v1/conversations/{id}{
"id": "conv_x8k2m9p4q7w1r3t",
"object": "conversation",
"created_at": 1760866000,
"model": "k3m9x2p7q1w8e4r",
"mode": "team",
"reply_pending": false
}model identifies the bound assistant. mode is "assistant" or "team". reply_pending indicates an outstanding assistant reply. Use these fields to resume polling when a page reopens.
Polling frequency
Poll every five seconds while the widget is open and the team owns the conversation. Stop in background tabs and closed widgets. On return, poll once to catch up.
A publishable key allows 20,000 reads/hour. One visitor polling every five seconds uses 720, supporting about 27 simultaneous waiting visitors per key. For larger audiences:
- Poll every five seconds for the first two minutes after handoff or the latest staff reply, then every 30–60 seconds.
- After 30 minutes without new messages, stop until the visitor writes or reopens the widget.
- On
429, wait forRetry-After.
See Limits.
// Read staff and assistant messages after the cursor, through all pages.
async function readTeamReplies(conversationId, cursor) {
const messages = [];
let mode = 'team';
let replyPending = true;
for (;;) {
const url = new URL(`https://pb.sonecentral.com/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`);
if (cursor) url.searchParams.set('after', cursor);
const response = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (!response.ok) break;
const page = await response.json();
messages.push(...page.data.filter((message) => message.role !== 'user'));
cursor = page.last_id ?? cursor;
mode = page.mode;
replyPending = page.reply_pending;
if (!page.has_more) break;
}
return { messages, cursor, mode, replyPending }; // stop only when mode is 'assistant' and replyPending is false
}Start the cursor at the last message ID from the last turn. Store and reuse the returned cursor after every poll. Complete example: Embedded chat widget.
When the assistant cannot reply
In an existing conversation, ai_turn_ceiling, assistant_quota_exhausted, plan_required or assistant_paused means the visitor message was stored and reached your team, but the assistant did not reply. Ownership remains mode: "assistant" until a staff member writes or shares a record. Their reply appears in the message list.
After these errors, keep polling even with assistant ownership until staff reply or the visitor's next message is answered.
The same errors on a first message store nothing and create no conversation. Ask the visitor to retry later.
Returning to the assistant
Staff can return ownership from Messages. mode becomes "assistant" immediately. If the last visitor message is unanswered, the assistant answers it too, without opening another HTTP stream. Continue reading while reply_pending is true and render both staff and assistant messages.
Stop when has_more is false, mode is "assistant" and reply_pending is false. Deduplicate by message ID against streamed messages; Vercel AI SDK's finish metadata messageIds exists for this purpose. The next visitor message is answered by the assistant again.