Try the interactive chat
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/completionsUses 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:
- Body
conversation_id. - Body
sone.conversation_id. X-Sone-Conversation-Idheader.metadata.conversation_id.sone.conversation_idfrom 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
| Role | Behavior |
|---|---|
user | Visitor message. content can be a string or an array of text parts. |
assistant | Previous reply. Used for conversation lookup; with a server key, also imported when initializing a new conversation. |
system, developer | With a server key, informational context, up to 2,000 characters. Ignored with publishable keys. See Developer context. |
tool, function | Ignored. |
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:
| Field | Error |
|---|---|
tools, functions | 400 tools_not_supported; the assistant uses the business's own tools. |
n other than 1 | 400 unsupported_parameter |
response_format other than text | 400 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
status | Meaning | content |
|---|---|---|
answered | The assistant replied. | Reply text |
handoff | The conversation moved to the team this turn and a message was sent, usually explaining the handoff. | Handoff message |
human | The team owns the conversation. The assistant did not reply; the visitor message reached the team. | Empty |
silent | No reply needed, for example after “thanks”. | Empty |
For handoff and human, read Human handoff and team replies.
Define these fields in TypeScript:
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:
- First chunk: after key, limit and plan checks pass and the assistant starts,
deltacarries the role. Rejected requests return normal HTTP errors instead. - Activity chunk: while a tool runs,
deltais empty and top-levelsone.activitycarries a short label, such as the Turkish “Ürünlere bakılıyor” (Checking products). - Text streams in word groups through
delta.content. Second and later messages begin with\n\n. Each message's final chunk also includes the fullSoneMessagein top-levelsone.message. A message with empty text uses one chunk with an emptydelta. - Final chunk:
finish_reasonis"stop",deltais empty, and top-levelsonecontainsconversation_idandstatus. - With
stream_options.include_usage, an extra chunk has emptychoicesand populatedusage; preceding chunks contain"usage": null. - 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
| Status | code | When |
|---|---|---|
400 | missing_user_message | The final message must be a user message containing text. |
400 | invalid_role | Unknown message role, or a Responses message item without a role. |
400 | invalid_content | Message content is not a string or an array of parts. |
400 | unsupported_content | A new message contains an image, audio or file. Send text only. |
400 | message_too_long | A new message exceeds 4,000 characters. Shorten it. |
400 | too_many_new_messages | More than 12 new visitor messages in one request. Include the assistant reply in history before continuing, or start a new conversation. |
400 | tools_not_supported | Remove tools, functions or function-call items. The assistant uses the business tools configured in Sonecentral. |
400 | unsupported_parameter | n is not 1, response_format or text.format is not text, or background is true. param identifies the field. |
400 | conversation_model_mismatch | The conversation belongs to another assistant. Use the same model or start a new conversation. |
404 | model_not_found | No published assistant with this ID is available to the key. GET /models lists available assistants. |
404 | conversation_not_found | Not an API conversation in this workspace, or its assistant is unavailable to the key. Clear the saved ID and start a new conversation. |
409 | conversation_busy | The previous turn is still running. This message was not stored. Wait and retry with the same Idempotency-Key. |
422 | idempotency_key_reused | The same Idempotency-Key was used with different text. Use a new key for each new message. |
All codes and recovery steps: Errors.