sone centraldocs
Try the interactive chat

Sample store

Our team is replying

Can I speak to someone on your team?

Of course. I am handing this chat over to our team.

Elif · Team

Hello, I am Elif. How can I help you?

Uses sample data; does not start a real conversation.

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:

statusOutcomeClient action
answeredThe assistant replied.None.
handoffThis turn moved the conversation to the team and sent a message, usually explaining the handoff.Display the message and start polling replies.
humanThe team owns the conversation. The assistant did not reply; the message reached the team.Start or continue polling replies.
silentNo reply needed.None.

status appears in each format:

  • Chat Completions: choices[0].message.sone.status for non-streamed responses, or top-level sone.status on the final stream chunk.
  • Responses: top-level sone.status.
  • Vercel AI SDK: messageMetadata.status on finish.

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

FieldMeaning
dataOldest-first SoneMessage array. Staff can also send products, services, documents and buttons.
has_moreMore messages exist. Fetch immediately using last_id.
mode"team" or "assistant". Assistant ownership does not imply the reply has finished.
reply_pendingThe assistant has a running, queued or recoverable reply. Continue polling even with mode: "assistant".
first_id, last_idIDs of the first and last messages on this page. Omitted when data is empty.
  • All roles are included: visitor user, assistant assistant, and team staff. Do not redraw visitor messages already shown.
  • Always advance after to response last_id. Visitor messages also advance the cursor, preventing repeated reads.
  • An after outside this conversation returns 400 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 itemtypeData
Product or servicecatalogOne record per staff share in catalog.items; kind is inventory for products and offering for services.
DocumentfileSigned 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 text for text-only clients. Long catalog fallback text may be shortened; catalog.items remains 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 for Retry-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.