sone centraldocs
Try the interactive chat

Sample store

Sone assistant

Hello! Ask me about linen shirts, shipping or our size guide.

Uses sample data; does not start a real conversation.

Chat Completions

Send messages using OpenAI Chat Completions. The sone fields carry conversation state and rich messages.

POST https://pb.sonecentral.com/api/v1/chat/completions

Uses OpenAI Chat Completions request and response formats. OpenAI SDKs, LangChain and compatible clients work by changing baseURL.

Request

{
  "model": "default",
  "messages": [
    { "role": "user", "content": "Are you open this weekend?" }
  ],
  "metadata": { "conversation_id": "conv_x8k2m9p4q7w1r3t" },
  "stream": false
}

Prop

Type

The first non-empty conversation ID is used in this order:

  1. Body conversation_id.
  2. Body sone.conversation_id.
  3. X-Sone-Conversation-Id header.
  4. metadata.conversation_id.
  5. sone.conversation_id from an assistant message returned unchanged in history, using the most recent if several exist.

Without an ID, other lookup methods apply: Conversation continuity.

Message roles

RoleBehavior
userVisitor message. content can be a string or an array of text parts.
assistantPrevious reply. Used for conversation lookup; with a server key, also imported when initializing a new conversation.
system, developerWith a server key, informational context, up to 2,000 characters. Ignored with publishable keys. See Developer context.
tool, functionIgnored.

Other roles return 400 invalid_role. The final message must be a text-bearing user message, otherwise 400 missing_user_message. A new message longer than 4,000 characters returns 400 message_too_long.

Images, audio and files in new messages return 400 unsupported_content. Such parts in previous messages are skipped, retaining only their text.

Ignored and unsupported fields

Sonecentral's assistant configuration determines the model, tone and tools. These fields are ignored without an error: temperature, top_p, max_tokens, max_completion_tokens, presence_penalty, frequency_penalty, seed, stop, logit_bias, logprobs, top_logprobs, reasoning_effort, verbosity, service_tier, store, parallel_tool_calls, tool_choice, function_call, audio, modalities. Unknown fields are also ignored.

These fields are rejected:

FieldError
tools, functions400 tools_not_supported; the assistant uses the business's own tools.
n other than 1400 unsupported_parameter
response_format other than text400 unsupported_parameter

Response

{
  "id": "chatcmpl-9f2c4a7e1b8d3f6a0c5e2b9d",
  "object": "chat.completion",
  "created": 1760866000,
  "model": "k3m9x2p7q1w8e4r",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Yes, we are open Saturday from 10:00 to 18:00. We are closed on Sunday.",
        "refusal": null,
        "annotations": [],
        "sone": {
          "conversation_id": "conv_x8k2m9p4q7w1r3t",
          "status": "answered",
          "messages": [
            {
              "id": "msg_p4q7w1x8k2m9a5z",
              "role": "assistant",
              "type": "text",
              "text": "Yes, we are open Saturday from 10:00 to 18:00. We are closed on Sunday.",
              "created_at": 1760866000
            }
          ]
        }
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 7, "completion_tokens": 16, "total_tokens": 23 },
  "system_fingerprint": null,
  "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered" }
}

model is the replying assistant's ID, even when the request uses "default". The conversation ID is also in X-Sone-Conversation-Id.

content is the plain text of all messages in the turn, separated by blank lines (\n\n). A text-only client remains fully functional.

usage is informational, estimated from new input and output text at roughly four characters per token. Billing is not based on these values.

The sone fields

These are additions to OpenAI's format. The complete object is at choices[0].message.sone. A summary without messages is also present at the response's top level.

Prop

Type

statusMeaningcontent
answeredThe assistant replied.Reply text
handoffThe conversation moved to the team this turn and a message was sent, usually explaining the handoff.Handoff message
humanThe team owns the conversation. The assistant did not reply; the visitor message reached the team.Empty
silentNo reply needed, for example after “thanks”.Empty

For handoff and human, read Human handoff and team replies.

Define these fields in TypeScript:

sone.ts
import type { SoneMessage } from './sone-message'; // definition from the Rich messages page

export type SoneStatus = 'answered' | 'handoff' | 'human' | 'silent';

// Non-streamed response: choices[0].message.sone
export type SoneTurn = {
  conversation_id: string;
  status: SoneStatus;
  messages: SoneMessage[];
};

// Top-level sone on a stream chunk; only one field is populated per chunk
export type SoneChunk = {
  message?: SoneMessage; // a message completed
  activity?: string; // current assistant activity: "Ürünlere bakılıyor"
  conversation_id?: string; // on the final chunk
  status?: SoneStatus; // on the final chunk
};

export const soneOfMessage = (message: unknown) => (message as { sone?: SoneTurn }).sone;
export const soneOfChunk = (chunk: unknown) => (chunk as { sone?: SoneChunk }).sone;

OpenAI's Python models preserve unknown fields:

sone = completion.choices[0].message.to_dict().get("sone", {})
print(sone.get("conversation_id"), sone.get("status"))

Streaming

With "stream": true, the response is text/event-stream. Chunks arrive in this order:

  1. First chunk: after key, limit and plan checks pass and the assistant starts, delta carries the role. Rejected requests return normal HTTP errors instead.
  2. Activity chunk: while a tool runs, delta is empty and top-level sone.activity carries a short label, such as the Turkish “Ürünlere bakılıyor” (Checking products).
  3. Text streams in word groups through delta.content. Second and later messages begin with \n\n. Each message's final chunk also includes the full SoneMessage in top-level sone.message. A message with empty text uses one chunk with an empty delta.
  4. Final chunk: finish_reason is "stop", delta is empty, and top-level sone contains conversation_id and status.
  5. With stream_options.include_usage, an extra chunk has empty choices and populated usage; preceding chunks contain "usage": null.
  6. The stream ends with data: [DONE].

sone is always top-level on the chunk, never inside choices[0].delta. The conversation ID is available in X-Sone-Conversation-Id when streaming begins.

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":null}],"sone":{"activity":"Çalışma saatlerine bakılıyor"}}

: ping

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"Yes, we are open Saturday "},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"from 10:00 to 18:00. We are "},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"closed on Sunday."},"logprobs":null,"finish_reason":null}],"sone":{"message":{"id":"msg_p4q7w1x8k2m9a5z","role":"assistant","type":"text","text":"Yes, we are open Saturday from 10:00 to 18:00. We are closed on Sunday.","created_at":1760866000}}}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"sone":{"conversation_id":"conv_x8k2m9p4q7w1r3t","status":"answered"}}

data: [DONE]

IDs in this example are shortened.

import type { SoneMessage } from './sone-message';
import { soneOfChunk } from './sone';

const stream = await client.chat.completions.create({
  model: 'default',
  messages: [{ role: 'user', content: 'Are you open this weekend?' }],
  stream: true,
});

const messages: SoneMessage[] = [];
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? '');
  const sone = soneOfChunk(chunk);
  if (sone?.message) messages.push(sone.message); // render the rich message here
  if (sone?.activity) console.log(`\n${sone.activity}…`);
  if (sone?.status) console.log('\nsohbet:', sone.conversation_id, sone.status);
}

An error after streaming begins arrives as data: {"error": {…}}, followed by data: [DONE]. OpenAI SDKs throw it as an error. See Errors.

For message-level delivery and keepalives, see Streaming.

Retries

Send a new Idempotency-Key, up to 200 characters, for every new message. Reuse it when retrying that message after a network failure. The message is stored once and the same reply is returned if already answered. Different text with the same key returns 422 idempotency_key_reused.

With a publishable key, a retry without a conversation ID from a different network address, such as a first-message retry, opens a new conversation: Retries.

OpenAI SDKs do not add this header automatically. Pass it in request options so SDK retries reuse it:

const key = crypto.randomUUID(); // generate once per message and reuse on retries

const completion = await client.chat.completions.create(
  {
    model: 'default',
    metadata: { conversation_id: 'conv_x8k2m9p4q7w1r3t' },
    messages: [{ role: 'user', content: 'Are you open this weekend?' }],
  },
  { headers: { 'Idempotency-Key': key } },
);

When every request includes the complete history and conversation ID, Sonecentral can also identify retries from the history tail after the first message. History alone without an ID or message identity cannot find an unanswered retry: it starts another conversation and replies again. An idempotency key is the safe path for first messages and clients sending only new text without saving an ID. Send one on every message. See Retries.

Errors

StatuscodeWhen
400missing_user_messageThe final message must be a user message containing text.
400invalid_roleUnknown message role, or a Responses message item without a role.
400invalid_contentMessage content is not a string or an array of parts.
400unsupported_contentA new message contains an image, audio or file. Send text only.
400message_too_longA new message exceeds 4,000 characters. Shorten it.
400too_many_new_messagesMore than 12 new visitor messages in one request. Include the assistant reply in history before continuing, or start a new conversation.
400tools_not_supportedRemove tools, functions or function-call items. The assistant uses the business tools configured in Sonecentral.
400unsupported_parametern is not 1, response_format or text.format is not text, or background is true. param identifies the field.
400conversation_model_mismatchThe conversation belongs to another assistant. Use the same model or start a new conversation.
404model_not_foundNo published assistant with this ID is available to the key. GET /models lists available assistants.
404conversation_not_foundNot an API conversation in this workspace, or its assistant is unavailable to the key. Clear the saved ID and start a new conversation.
409conversation_busyThe previous turn is still running. This message was not stored. Wait and retry with the same Idempotency-Key.
422idempotency_key_reusedThe same Idempotency-Key was used with different text. Use a new key for each new message.

All codes and recovery steps: Errors.