# Sonecentral text assistant API: full documentation > Call your Sonecentral text assistant from your own app or website. Supports OpenAI Chat Completions, OpenAI Responses and Vercel AI SDK formats, with the same assistant that replies on WhatsApp. Source: https://docs.sonecentral.com/en. Published at https://docs.sonecentral.com/en/llms-full.txt and regenerated on each build. Pages follow the sidebar order; each starts with its title and address. --- # Introduction Page: https://docs.sonecentral.com/en Markdown: https://docs.sonecentral.com/en/index.md > Call your Sonecentral text assistant from your own app or website using familiar OpenAI and Vercel AI SDK formats. > **Give the docs to your agent.** The full documentation is one Markdown file: https://docs.sonecentral.com/en/llms-full.txt. Paste this URL into your coding agent. For one page, use its .md address, for example https://docs.sonecentral.com/en/hizli-baslangic.md. Page index: https://docs.sonecentral.com/en/llms.txt ## What this API does The text assistant you configure in Sonecentral replies to customers on WhatsApp, Instagram, Messenger and TikTok. This API brings that same assistant into your app, website or backend. **Your assistant** replies, with the instructions and workspace configuration you already use: services, products, stock, catalog items, team, departments, business hours, business information, campaigns, shipping rules, price lists, discount authority, Knowledge Base documents and business data extracted from them, order and appointment requests, and team handoff. Requests run through the same runtime that handles your WhatsApp messages. The difference is that API visitors are anonymous. The assistant does not access their personal records or confirm appointments itself. Every order and appointment is a request awaiting your team's approval. ## Base URL and formats All requests use this base URL: ```txt https://pb.sonecentral.com/api/v1 ``` Choose one of three formats. All use the same assistant and conversation record. | Endpoint | Format and use case | | --- | --- | | `POST /chat/completions` | OpenAI Chat Completions, for an existing OpenAI SDK or compatible client. | | `POST /responses` | OpenAI Responses, to continue with `previous_response_id`. | | `POST /chat` | Vercel AI SDK, for a React or Next.js chat built with `useChat`. | Set `model` to your assistant's ID. `GET /models` lists IDs: [Assistants and models](https://docs.sonecentral.com/en/asistanlar-ve-modeller.md). If your key allows one assistant, use `"default"`. ## Things to know - **Visitors are anonymous.** The API does not verify a visitor's identity. The assistant helps with public information such as products, prices, stock and appointment availability, and does not disclose previous orders or existing appointments. See [Orders, appointments and privacy](https://docs.sonecentral.com/en/siparis-randevu-gizlilik.md). - **Streaming delivers messages.** Business safety checks inspect the complete reply before text is sent. Streaming progresses message by message. See [Streaming](https://docs.sonecentral.com/en/akis.md). - **Rich messages include plain text.** Product cards, buttons and files arrive as structured data, with a plain text fallback for each. See [Rich messages](https://docs.sonecentral.com/en/zengin-mesajlar.md). - **Conversations appear in Messages.** Every API conversation appears with source **API** and visitor name **API ziyaretçisi** (API visitor). The assistant can hand it to your team; staff reply there and your client reads their replies through the API. See [Human handoff and team replies](https://docs.sonecentral.com/en/insan-devri.md). ## Endpoints | Endpoint | Description | | --- | --- | | `GET /models` | Assistants available to the key. [Assistants and models](https://docs.sonecentral.com/en/asistanlar-ve-modeller.md) | | `GET /models/{id}` | One assistant. | | `POST /chat/completions` | OpenAI Chat Completions, streamed or non-streamed. [Chat Completions](https://docs.sonecentral.com/en/chat-completions.md) | | `POST /responses` | OpenAI Responses, streamed or non-streamed. [Responses API](https://docs.sonecentral.com/en/responses.md) | | `GET /responses/{id}` | A stored response. | | `DELETE /responses/{id}` | Delete a stored response. | | `GET /responses/{id}/input_items` | A response's input items. | | `POST /responses/{id}/cancel` | Always returns `400 not_cancellable` for an existing response, or `404 response_not_found`; responses complete within the request. | | `POST /chat` | Vercel AI SDK UI message stream for `useChat`. [Vercel AI SDK](https://docs.sonecentral.com/en/vercel-ai-sdk.md) | | `GET /chat/{id}/stream` | For `useChat({ resume: true })`. Always returns `204`. [Disconnections](https://docs.sonecentral.com/en/vercel-ai-sdk.md#baglanti-koparsa) | | `GET /conversations/{id}` | Whether the assistant or team owns the conversation. [Conversation state](https://docs.sonecentral.com/en/insan-devri.md#sohbetin-durumu) | | `GET /conversations/{id}/messages` | Conversation messages, including team replies. [Human handoff](https://docs.sonecentral.com/en/insan-devri.md) | | `GET /files/{id}` | Signed download URL for a file shared by the assistant. No API key required. [Downloading files](https://docs.sonecentral.com/en/zengin-mesajlar.md#dosya-indirme) | | `GET /key` | Summary of the current key. [Authentication](https://docs.sonecentral.com/en/kimlik-dogrulama.md#anahtari-denetlemek) | All endpoints answer `OPTIONS` preflight requests, so a browser can call them with a publishable key. Every endpoint except `GET /files/{id}` requires a key and counts toward its [limits](https://docs.sonecentral.com/en/limitler.md). ## Next step Create your key and send your first request in the [Quickstart](https://docs.sonecentral.com/en/hizli-baslangic.md). --- # Quickstart Page: https://docs.sonecentral.com/en/hizli-baslangic Markdown: https://docs.sonecentral.com/en/hizli-baslangic.md > Create a key, find your assistant's ID and send your first message. > **Give the docs to your agent.** The full documentation is one Markdown file: https://docs.sonecentral.com/en/llms-full.txt. Paste this URL into your coding agent. For one page, use its .md address, for example https://docs.sonecentral.com/en/hizli-baslangic.md. Page index: https://docs.sonecentral.com/en/llms.txt Working with a coding agent? Give it the URL above and describe what you want to build. To implement it yourself, follow these four steps. ### Create a key In Sonecentral, open **Metin asistanı** (Text assistant) and create a key under **API anahtarları** (API keys). Only workspace administrators see this section. **Tür** (Type) offers two choices: - **Server key** starts with `sk-sone-v1-` and belongs in your backend. Never put it in a browser, mobile app bundle or public repository. - **Publishable key** starts with `pk-sone-v1-` and is for your website's chat widget. It works only from your allowed origins. The key is shown once, when created. Save it immediately as an environment variable: ```bash export SONE_API_KEY="sk-sone-v1-..." ``` Details: [Authentication and keys](https://docs.sonecentral.com/en/kimlik-dogrulama.md). ### Find your assistant's ID ```bash curl https://pb.sonecentral.com/api/v1/models \ -H "Authorization: Bearer $SONE_API_KEY" ``` Use the listed `id` as `model`. If the key allows only one assistant, `"default"` is sufficient. ### Send the first message **curl** ```bash curl https://pb.sonecentral.com/api/v1/chat/completions \ -H "Authorization: Bearer $SONE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "default", "messages": [ { "role": "user", "content": "Can I book an appointment tomorrow afternoon?" } ] }' ``` **OpenAI JS** ```ts import OpenAI from 'openai'; const client = new OpenAI({ baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); const completion = await client.chat.completions.create({ model: 'default', messages: [{ role: 'user', content: 'Can I book an appointment tomorrow afternoon?' }], }); console.log(completion.choices[0].message.content); ``` **OpenAI Python** ```python import os from openai import OpenAI client = OpenAI( base_url="https://pb.sonecentral.com/api/v1", api_key=os.environ["SONE_API_KEY"], ) completion = client.chat.completions.create( model="default", messages=[{"role": "user", "content": "Can I book an appointment tomorrow afternoon?"}], ) print(completion.choices[0].message.content) ``` **Vercel AI SDK** ```ts import { createOpenAI } from '@ai-sdk/openai'; import { generateText } from 'ai'; const sone = createOpenAI({ baseURL: 'https://pb.sonecentral.com/api/v1', apiKey: process.env.SONE_API_KEY, }); const { text } = await generateText({ model: sone.chat('default'), prompt: 'Can I book an appointment tomorrow afternoon?', }); console.log(text); ``` Install with `npm i openai`, `pip install openai` or `npm i ai @ai-sdk/openai`. Examples were tested with `openai` 7 (Node), `openai` 2 (Python), `ai` 7 and `@ai-sdk/openai` 4. ### Continue the conversation The response includes `choices[0].message.sone.conversation_id`. Send it back in `metadata` on the next request so the assistant remembers earlier messages. When sending only the new message, add an `Idempotency-Key` to each message: retrying after a network failure with the same key prevents duplicate storage. ```ts // OpenAI types omit `sone`; see the Chat Completions page for its type. const { sone } = completion.choices[0].message as unknown as { sone: { conversation_id: string }; }; await client.chat.completions.create( { model: 'default', metadata: { conversation_id: sone.conversation_id }, messages: [{ role: 'user', content: 'Is four oclock available?' }], }, { headers: { 'Idempotency-Key': crypto.randomUUID() } }, ); ``` Other continuation methods, retries and browser persistence: [Conversation continuity](https://docs.sonecentral.com/en/sohbet-surekliligi.md). ## Where to go next - Add a chat to your website: [Embedded chat widget](https://docs.sonecentral.com/en/sohbet-penceresi.md) - React and Next.js: [Vercel AI SDK](https://docs.sonecentral.com/en/vercel-ai-sdk.md) - Product cards and buttons: [Rich messages](https://docs.sonecentral.com/en/zengin-mesajlar.md) --- # Authentication and keys Page: https://docs.sonecentral.com/en/kimlik-dogrulama Markdown: https://docs.sonecentral.com/en/kimlik-dogrulama.md > Server and publishable keys, authentication headers, allowed origins and revocation. ## Two key types | | Server key | Publishable key | | --- | --- | --- | | Format | `sk-sone-v1-` followed by 48 letters or digits | `pk-sone-v1-` followed by 48 letters or digits | | Use | Backend, serverless function or script | Website chat widget | | From a browser | Rejected (`403 secret_key_in_browser`) | Allowed origins only | | Find conversations with `user` | Supported | Ignored | | Developer context | Supported | Ignored | | History imported into a new conversation | Visitor and assistant messages | Visitor messages only | | Request limit | 120 per minute per key | 30 per minute per visitor address; 1,200 requests and 20,000 reads per hour per key | | Automatic reply limit | Workspace total: 60 turns/hour, 300/day | Shared across all publishable keys within that total: 40 turns/hour, 200/day | Create, edit and revoke keys under **Metin asistanı → API anahtarları** (Text assistant → API keys) in Sonecentral. Only workspace administrators see this section. A workspace can have up to 50 active keys; revoked keys do not count. ## Send the key Use the `Authorization` header: ```http Authorization: Bearer sk-sone-v1-... ``` `x-api-key` is also accepted. If both are supplied, `Authorization` takes precedence. ```http x-api-key: sk-sone-v1-... ``` OpenAI SDKs and Vercel AI SDK send their `apiKey` option as `Authorization: Bearer` automatically. ## When the key is shown The complete key is shown once, at creation. Sonecentral stores only its hash. The list displays its prefix and final four characters. Request logs do not retain the key, whether sent through `Authorization` or `x-api-key`. If lost, create a new key and revoke the old one. ## Rotate a key Open [Text assistant → API keys](https://app.sonecentral.com/text-agent#api-anahtarlari) as a workspace administrator. Create a replacement with the required assistant scope, allowed origins and request permission. Copy it once, update your integration and verify it with `GET /key`, then revoke the old key. Rotation uses create-and-revoke; there is no separate rotate button. If a key is exposed, revoke it immediately. Key-management responses use `Cache-Control: no-store`. Requests carrying `Origin: null` (for example from an opaque or sandboxed browser context) are refused; server requests omit `Origin`. ## Key settings - **Name.** Up to 80 characters to identify the key. Also returned by `GET /key`; do not include secrets. - **Type.** Server or publishable. Cannot be changed after creation. - **Assistants.** Which assistants the key can call. Selecting all includes every published workspace assistant, including future ones. You can select an unpublished assistant; the key starts working with it once published. - **Allowed origins.** Publishable keys only, from 1 to 20 origins. - **Can receive order and appointment requests.** Enabled by default. The assistant can create requests awaiting your team's approval. When disabled, it passes the details to your team instead. See [Orders, appointments and privacy](https://docs.sonecentral.com/en/siparis-randevu-gizlilik.md). All settings except type can be edited later. Changes apply on the next request. ## Allowed origins Browsers send `Origin` automatically on cross-origin requests. A publishable key is accepted only when `Origin` matches its allowlist. It cannot be used without an `Origin` header. - Use a scheme and hostname, such as `https://www.example.com`. The hostname must contain at least one dot. For a non-443 port, use `https://www.example.com:8443`. - Paths, query strings, `*` wildcards, IP addresses and `https://localhost` are not accepted. - `https://example.com` and `https://www.example.com` are separate origins. Add both if needed. - For development, only `http://localhost` and `http://127.0.0.1` are allowed over HTTP, with or without a port, such as `http://localhost:3000` or `http://127.0.0.1:5173`. Hostnames are compared in lowercase. An origin outside the allowlist returns `403 origin_not_allowed`. ## Server keys do not belong in browsers Anyone can read a key embedded in website code and make requests on your behalf. Sonecentral rejects server-key requests carrying an origin not listed on that key with `403 secret_key_in_browser`. The dashboard does not add origins to server keys, so browser-origin requests are rejected. Always use a publishable key in browsers. This check is a safety net. Pages opened from a local file or a sandboxed frame can send `Origin: null`; Sonecentral rejects these requests too. A client outside the browser can omit or forge `Origin`, so this check does not protect an exposed key. Never include a server key in a browser, mobile app bundle or public repository. > **Warning** > > The OpenAI SDK's `dangerouslyAllowBrowser` option does not bypass this rule. Use a publishable key in a browser. Browser code can read these response headers: `x-request-id`, `retry-after`, `x-should-retry` and `x-sone-conversation-id`. ## Developer context With a server key, `system` and `developer` messages, Responses `instructions`, and Vercel AI SDK `context` are passed to the assistant **as information only**. For example, identify the product page the visitor is viewing or their app language preference. This text cannot override business instructions or rules; instructions embedded in it are not followed. Multiple contexts are joined and limited to 2,000 characters. Responses rejects `instructions` above 2,000 characters or 4 KB of JSON with `400 instructions_too_long`. Context applies only to the current turn; send it again if needed on the next request. Context sent with a publishable key is ignored and not stored, because visitors can change it. ## Revocation Revoking a key in **API anahtarları** disables it immediately. Subsequent requests return `401 revoked_api_key`. Revocation cannot be undone and revoked keys cannot be edited. Existing conversations remain in Messages. Stored responses created by a revoked key expire after 30 days. A new key that can call the same assistant can read them and continue with `previous_response_id`, but cannot delete them or see `instructions`, `safety_identifier` or `user`. See [Stored responses](https://docs.sonecentral.com/en/responses.md#saklanan-yanitlar). ## Inspect the key `GET /key` returns the current key's summary. Use it to check the workspace and available assistants during setup. ```bash curl https://pb.sonecentral.com/api/v1/key \ -H "Authorization: Bearer $SONE_API_KEY" ``` ```json { "data": { "label": "pk-sone-v1-Ab3…", "name": "Web sitesi sohbeti", "kind": "publishable", "assistants": ["k3m9x2p7q1w8e4r"], "requests": true, "allowed_origins": ["https://www.ornek.com"], "limits": { "requests_per_minute_per_visitor": 30, "requests_per_hour": 1200, "read_requests_per_hour": 20000, "assistant_turns_per_hour": 60, "assistant_turns_per_day": 300, "browser_assistant_turns_per_hour": 40, "browser_assistant_turns_per_day": 200 } } } ``` | Field | Type | Description | | --- | --- | --- | | `label` | string | First 14 characters followed by an ellipsis. No response includes the complete key. | | `name` | string | Name assigned in Sonecentral. | | `kind` | "secret" \| "publishable" | secret is a server key; publishable is a browser key. | | `assistants` | string[] | Currently published, allowed assistant IDs, sorted by assistant name. | | `requests` | boolean | Whether order and appointment requests are enabled. | | `allowed_origins` | string[] | Allowed origins. Empty for server keys. | | `limits` | object | Server keys include requests_per_minute. Publishable keys include requests_per_minute_per_visitor, requests_per_hour (non-read requests), read_requests_per_hour, browser_assistant_turns_per_hour and browser_assistant_turns_per_day. Both include the workspace's assistant_turns_per_hour and assistant_turns_per_day. | Server-key `limits`: ```json { "requests_per_minute": 120, "assistant_turns_per_hour": 60, "assistant_turns_per_day": 300 } ``` `GET /key` is a read and counts toward the key's [limits](https://docs.sonecentral.com/en/limitler.md). ## Errors | Status | `code` | When | | --- | --- | --- | | `401` | `missing_api_key` | No key supplied. | | `401` | `invalid_api_key` | Unknown or malformed key, or prefix does not match its type. | | `401` | `revoked_api_key` | Key has been revoked. | | `403` | `origin_not_allowed` | Publishable key used outside its allowlist or without `Origin`. | | `403` | `secret_key_in_browser` | Server key used from an origin not listed on the key. | | `429` | `rate_limit_exceeded` | Request or read limit exceeded. Wait for `Retry-After`. | All codes: [Errors](https://docs.sonecentral.com/en/hatalar.md). --- # Assistants and models Page: https://docs.sonecentral.com/en/asistanlar-ve-modeller Markdown: https://docs.sonecentral.com/en/asistanlar-ve-modeller.md > What to put in model, how to list assistants and what "default" means. In OpenAI's format, `model` names a language model. Here it is **your assistant's ID** and selects which assistant replies. Its Sonecentral settings determine the language model, instructions and tools; requests cannot change them. ## What the assistant uses The API assistant is the same assistant that replies on WhatsApp, Instagram, Messenger and TikTok. It uses your workspace configuration in the same way: - Services, with prices, durations, images and links. - Products and stock, with prices, images and links. - Catalog items and packages. - Team, departments and business hours. - Business name, address and phone number. - Your connected store's shipping rules and campaigns. - Price lists and any configured discount authority. - Knowledge Base documents, documents marked as shareable, and business data extracted from your documents. - Order and appointment requests and appointment availability, if the key enables requests. - Notes for the team and conversation handoff. TikTok is an officially supported messaging channel. Its native message formats differ from the API and Meta channels: [Channels](https://docs.sonecentral.com/en/kanallar.md). Product and service cards, buttons and files arrive through this API as structured data: [Rich messages](https://docs.sonecentral.com/en/zengin-mesajlar.md). Because API visitors are not authenticated, the assistant does not read or change their personal records (previous orders, existing appointments, customer profile, remembered information or connected panel account), and does not confirm appointments itself. See [Orders, appointments and privacy](https://docs.sonecentral.com/en/siparis-randevu-gizlilik.md). ## List assistants ```bash curl https://pb.sonecentral.com/api/v1/models \ -H "Authorization: Bearer $SONE_API_KEY" ``` ```json { "object": "list", "data": [ { "id": "k3m9x2p7q1w8e4r", "object": "model", "created": 1760889600, "owned_by": "sonecentral", "name": "Store assistant" } ] } ``` The list contains only **published assistants the key can call**. Drafts and assistants with incomplete setup are excluded. Results are sorted by assistant name. | Field | Type | Description | | --- | --- | --- | | `id` | string | Assistant ID. Use this as model. | | `object` | "model" | Always model. | | `created` | integer | Assistant creation time in Unix seconds. | | `owned_by` | "sonecentral" | Always sonecentral. | | `name` | string | Assistant name in the dashboard. | `GET /models/{id}` returns the same object for one assistant. ```bash curl https://pb.sonecentral.com/api/v1/models/k3m9x2p7q1w8e4r \ -H "Authorization: Bearer $SONE_API_KEY" ``` In OpenAI SDKs, use `client.models.list()` and `client.models.retrieve(id)`. Everything before the final `/` in `model` is ignored. Tools that prefix model names with a provider can therefore use `sonecentral/k3m9x2p7q1w8e4r` to select the same assistant. ## `"default"` When `model` is omitted or set to `"default"`, selection follows this order: 1. The key's only allowed assistant, if there is exactly one. 2. Otherwise, the workspace's default text assistant, if the key allows it. 3. Otherwise, the first allowed assistant in alphabetical name order. If your key allows several assistants and the choice matters, send an explicit ID. ## Conversation and assistant A conversation is bound to the assistant that started it. Continuing it with another `model` returns `400 conversation_model_mismatch`. Use the same assistant or start a new conversation. ## Errors | Status | `code` | When | | --- | --- | --- | | `404` | `model_not_found` | No published assistant with this ID is available to the key. Also returned for drafts and incomplete setup. | | `400` | `conversation_model_mismatch` | The conversation belongs to another assistant. | --- # Chat Completions Page: https://docs.sonecentral.com/en/chat-completions Markdown: https://docs.sonecentral.com/en/chat-completions.md > Send messages using OpenAI Chat Completions. The sone fields carry conversation state and rich messages. ```http 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 ```json { "model": "default", "messages": [ { "role": "user", "content": "Are you open this weekend?" } ], "metadata": { "conversation_id": "conv_x8k2m9p4q7w1r3t" }, "stream": false } ``` | Field | Type | Description | | --- | --- | --- | | `model` | string | Assistant ID or "default". See Assistants and models for IDs. Required. | | `messages` | array | Conversation messages. User messages after the final assistant message are new, up to 12. Earlier messages are used to find the conversation or initialize a new one. Required. | | `stream` | boolean | Stream the reply using server-sent events (SSE). Default: false. | | `stream_options` | object | include_usage: true adds usage at the end of the stream. | | `conversation_id` | string | Sonecentral extension: ID of the conversation to continue. | | `metadata` | object | Only metadata.conversation_id is read, as an alternative to conversation_id. Other metadata is ignored and not stored. | | `user` | string | Server keys only. An internal visitor identifier; the same value finds the same conversation. | | `safety_identifier` | string | Same function as user; takes precedence when both are supplied. | 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](https://docs.sonecentral.com/en/sohbet-surekliligi.md#sohbet-nasil-bulunur). ### 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](https://docs.sonecentral.com/en/kimlik-dogrulama.md#gelistirici-baglami). | | `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 ```json { "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. | Field | Type | Description | | --- | --- | --- | | `conversation_id` | string | Conversation ID, prefixed with conv_. Send it on subsequent requests. | | `status` | "answered" \| "handoff" \| "human" \| "silent" | Turn outcome, described below. | | `messages` | SoneMessage[] | Messages sent in this turn, in order. See Rich messages for the format. | | `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](https://docs.sonecentral.com/en/insan-devri.md). Define these fields in TypeScript: ```ts title="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: ```python 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. ```txt 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. **OpenAI JS** ```ts 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); } ``` **OpenAI Python** ```python stream = client.chat.completions.create( model="default", messages=[{"role": "user", "content": "Are you open this weekend?"}], stream=True, ) messages = [] for chunk in stream: if chunk.choices: print(chunk.choices[0].delta.content or "", end="", flush=True) sone = chunk.to_dict().get("sone") or {} if "message" in sone: messages.append(sone["message"]) # render the rich message here elif "activity" in sone: print(f"\n{sone['activity']}…") elif "status" in sone: print("\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](https://docs.sonecentral.com/en/hatalar.md#akis-sirasinda-hata). For message-level delivery and keepalives, see [Streaming](https://docs.sonecentral.com/en/akis.md). ## 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](https://docs.sonecentral.com/en/sohbet-surekliligi.md#tekrar-denemek). OpenAI SDKs do not add this header automatically. Pass it in request options so SDK retries reuse it: **OpenAI JS** ```ts 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 } }, ); ``` **OpenAI Python** ```python import uuid key = str(uuid.uuid4()) # generate once per message and reuse on retries completion = client.chat.completions.create( model="default", metadata={"conversation_id": "conv_x8k2m9p4q7w1r3t"}, messages=[{"role": "user", "content": "Are you open this weekend?"}], extra_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](https://docs.sonecentral.com/en/sohbet-surekliligi.md#tekrar-denemek). ## 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](https://docs.sonecentral.com/en/hatalar.md). --- # Responses API Page: https://docs.sonecentral.com/en/responses Markdown: https://docs.sonecentral.com/en/responses.md > Use OpenAI Responses, continue with previous_response_id or conversation, and read or delete stored responses. ```http POST https://pb.sonecentral.com/api/v1/responses ``` Uses OpenAI's Responses format. Continue by sending the previous response ID, without resending the entire history. When a turn contains several messages, `output_text` parts and stream deltas separate them with two newlines. This keeps the SDK's `output_text` or AI SDK's `text` readable. Each item's `sone.text` retains the original message. ## First message and continuation **OpenAI JS** ```ts const first = await client.responses.create({ model: 'default', input: 'I would like to book a hair coloring appointment.', }); console.log(first.output_text); const second = await client.responses.create({ model: 'default', previous_response_id: first.id, input: 'Would Thursday afternoon work?', }); console.log(second.output_text); ``` **OpenAI Python** ```python first = client.responses.create( model="default", input="I would like to book a hair coloring appointment.", ) print(first.output_text) second = client.responses.create( model="default", previous_response_id=first.id, input="Would Thursday afternoon work?", ) print(second.output_text) ``` **curl** ```bash curl https://pb.sonecentral.com/api/v1/responses \ -H "Authorization: Bearer $SONE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "default", "input": "I would like to book a hair coloring appointment." }' ``` `output_text` is an SDK convenience that joins every `output_text` part. ## Request | Field | Type | Description | | --- | --- | --- | | `model` | string | Assistant ID or "default". Required. | | `input` | string \| array | New message as text or OpenAI input items: message items with roles and item_reference items. input_text and output_text parts are read. Each item_reference ID is limited to 64 characters. The final item must be a visitor text message. Required. | | `previous_response_id` | string | Previous response ID: resp_ followed by 32 characters. Continues the same conversation. Cannot be combined with conversation. Longer values are rejected. | | `conversation` | string \| { id: string } | Conversation ID, sone.conversation_id. An alternative to previous_response_id. | | `store` | boolean | false disables response storage: GET /responses/{id} and previous_response_id cannot find it. Default: true. | | `instructions` | string | Server keys only: informational developer context, at most 2,000 characters and 4 KB JSON. Longer values are rejected. Ignored, not stored and omitted from responses for publishable keys. On stored reads, visible only to the storing key. | | `stream` | boolean | Stream the response as events. Default: false. | | `user` | string | Server keys only: internal visitor identifier for conversation lookup, at most 64 characters. Longer values are rejected. Not returned or stored. | | `safety_identifier` | string | Same lookup as user, taking precedence when both are supplied. At most 64 characters. Returned and stored for server keys; ignored, not stored and omitted for publishable keys. | | `metadata` | object | Echoed unchanged; not used for conversation lookup. Stored reads expose it only to the storing key. Up to 16 keys, each key 64 characters, each string value 512 characters, and 4 KB JSON total. | You can use `X-Sone-Conversation-Id` instead of `conversation`. Combining `conversation` and `previous_response_id` returns `400 conflicting_parameters`. Input `system` and `developer` messages are [developer context](https://docs.sonecentral.com/en/kimlik-dogrulama.md#gelistirici-baglami), like `instructions`. Returned assistant messages and `item_reference` items help find the conversation: [Conversation continuity](https://docs.sonecentral.com/en/sohbet-surekliligi.md#sohbet-nasil-bulunur). Model settings such as `temperature`, `top_p`, `max_output_tokens`, `reasoning`, `text.verbosity`, `tool_choice` and `truncation` are ignored. `max_output_tokens` is echoed in the response but does not change reply length. These inputs are rejected: | Input | Error | | --- | --- | | `tools` or function-call items such as `function_call` | `400 tools_not_supported` | | `background: true` | `400 unsupported_parameter` | | `text.format` other than `text` | `400 unsupported_parameter` | | Image or file in a new message | `400 unsupported_content` | | Unknown input item type | `400 unsupported_item` | `reasoning` items are silently skipped. ## Response ```json { "id": "resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f", "object": "response", "created_at": 1760866200, "completed_at": 1760866203, "status": "completed", "model": "k3m9x2p7q1w8e4r", "previous_response_id": null, "conversation": { "id": "conv_x8k2m9p4q7w1r3t" }, "output": [ { "type": "message", "id": "msg_w8e4rk3m9x2pq7a", "status": "completed", "role": "assistant", "content": [ { "type": "output_text", "text": "Of course. Which day works for you?", "annotations": [], "logprobs": [] } ], "sone": { "id": "msg_w8e4rk3m9x2pq7a", "role": "assistant", "type": "text", "text": "Of course. Which day works for you?", "created_at": 1760866203 } } ], "usage": { "input_tokens": 10, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens": 8, "output_tokens_details": { "reasoning_tokens": 0 }, "total_tokens": 18 }, "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered" } } ``` This example is abbreviated; actual responses also include OpenAI's other defined fields. `usage` is estimated from text length as described in [Chat Completions](https://docs.sonecentral.com/en/chat-completions.md). Each assistant message is a separate `message` output item. Its `id` is the Sonecentral message ID. Product cards, buttons and files include a plain text fallback in `content` and structured data in the item's `sone` field. See [Rich messages](https://docs.sonecentral.com/en/zengin-mesajlar.md). Top-level `sone` contains the conversation ID and turn outcome. `status` values match [Chat Completions](https://docs.sonecentral.com/en/chat-completions.md). `human` and `silent` turns have empty `output`. The conversation ID is also returned in `X-Sone-Conversation-Id`. ## Stored responses Responses are stored by default unless `store` is false. Stored responses expire after 30 days. The conversation itself remains in Messages. | Endpoint | Behavior | | --- | --- | | `GET /responses/{id}` | Returns the response in the same format. | | `GET /responses/{id}/input_items` | Lists this request's own input items, newest first by default; `?order=asc` returns oldest first. Single page, always `has_more: false`. | | `DELETE /responses/{id}` | Deletes the stored response: `{ "id": "resp_…", "object": "response.deleted", "deleted": true }`. Only the storing key can delete. Does not delete the conversation. | | `POST /responses/{id}/cancel` | Always returns `400 not_cancellable` if the response exists. Responses complete inside the request; no background task exists. | Only the request's own input items are retained: new visitor messages after the final assistant item, `item_reference`, and server-key `system`/`developer` messages. Publishable-key context is ignored and not stored. Replayed history is not stored again; the conversation retains it. Storage keeps the newest items, up to 40 items and 8 KB total JSON. The newest item is always retained, truncating its text from the end if needed to fit. Each item's text is limited to 4,000 characters. Items without IDs receive positional IDs such as `resp_…_in_0`. Stored request fields are bounded: `instructions` at 2,000 characters, `metadata` at 16 keys, `safety_identifier` and `user` at 64 characters, and each `item_reference` ID at 64 characters. `instructions` and `metadata` also have a 4 KB JSON limit; multi-byte text can exceed it within the character limit. Oversized requests return `400` and store nothing. `previous_response_id` is trimmed before storage; longer-than-ID values return `400 previous_response_id_too_long`. Retrying with the same `Idempotency-Key`, or the same `previous_response_id` and `input`, returns the original response with the same ID. It neither generates another reply nor stores another response. This also applies while the conversation belongs to the team. Any key allowed to call the response's assistant can read the response and input items by ID and continue with `previous_response_id`. An unavailable assistant's response appears absent: `404 response_not_found`, or `404 previous_response_not_found` for continuation. Only the storing key sees developer context, `metadata` and the submitted end-user identifier. Other keys see `instructions`, `safety_identifier` and `user` as `null`, empty `metadata` (`{}`), and no `system` or `developer` input items. Only the storing key can delete; others receive `404 response_not_found`. After rotation and revocation, old stored responses expire after 30 days. Response IDs are hard to guess, but keep them private. ```ts const stored = await client.responses.retrieve('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); const items = await client.responses.inputItems.list('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); await client.responses.delete('resp_7c1e9a4f2b8d6e3a0f5c9b2d7e4a1c8f'); ``` ## Streaming With `"stream": true`, events follow OpenAI's sequence. Each event has a `sequence_number` starting at 0 and increasing by one. Every assistant message opens and closes its own item: ```txt event: response.created data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_7c1e","status":"in_progress","conversation":{"id":"conv_x8k2m9p4q7w1r3t"},"output":[]}} event: response.in_progress data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_7c1e","status":"in_progress"}} : sone.activity {"label":"Uygun saatlere bakılıyor"} event: response.output_item.added data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"type":"message","id":"msg_w8e4","status":"in_progress","role":"assistant","content":[],"sone":{…}}} event: response.content_part.added data: {"type":"response.content_part.added","sequence_number":3,"item_id":"msg_w8e4","output_index":0,"content_index":0,"part":{"type":"output_text","text":"","annotations":[],"logprobs":[]}} event: response.output_text.delta data: {"type":"response.output_text.delta","sequence_number":4,"item_id":"msg_w8e4","output_index":0,"content_index":0,"delta":"Of course. Which day works ","logprobs":[]} event: response.output_text.delta data: {"type":"response.output_text.delta","sequence_number":5,"item_id":"msg_w8e4","output_index":0,"content_index":0,"delta":"for you?","logprobs":[]} event: response.output_text.done data: {"type":"response.output_text.done","sequence_number":6,"item_id":"msg_w8e4","output_index":0,"content_index":0,"text":"Of course. Which day works for you?","logprobs":[]} event: response.content_part.done data: {"type":"response.content_part.done","sequence_number":7,"item_id":"msg_w8e4","output_index":0,"content_index":0,"part":{"type":"output_text","text":"Of course. Which day works for you?","annotations":[],"logprobs":[]}} event: response.output_item.done data: {"type":"response.output_item.done","sequence_number":8,"output_index":0,"item":{"type":"message","id":"msg_w8e4","status":"completed","role":"assistant","content":[{"type":"output_text","text":"Of course. Which day works for you?","annotations":[],"logprobs":[]}],"sone":{…}}} event: response.completed data: {"type":"response.completed","sequence_number":9,"response":{"id":"resp_7c1e","status":"completed","sone":{"conversation_id":"conv_x8k2m9p4q7w1r3t","status":"answered"}}} ``` Objects are abbreviated here; actual events contain all fields. `: sone.activity` is an SSE comment, not an event. Its `label` is a short Turkish activity description, such as “Uygun saatlere bakılıyor” (Checking available times), and it has no `sequence_number`. OpenAI SDKs skip comments. To display activity, parse lines beginning with `: sone.activity ` as JSON after the prefix. `: ping` lines are comments too. **OpenAI JS** ```ts import OpenAI from 'openai'; const stream = await client.responses.create({ model: 'default', input: 'I would like to book a hair coloring appointment.', stream: true, }); try { for await (const event of stream) { if (event.type === 'response.output_text.delta') process.stdout.write(event.delta); if (event.type === 'response.output_item.done') process.stdout.write('\n'); } } catch (error) { // Post-start failure: the SDK throws APIError for the error event. if (error instanceof OpenAI.APIError) console.error(error.code, error.message); else throw error; } ``` **OpenAI Python** ```python stream = client.responses.create( model="default", input="I would like to book a hair coloring appointment.", stream=True, ) for event in stream: if event.type == "response.output_text.delta": print(event.delta, end="", flush=True) elif event.type == "response.output_item.done": print() elif event.type == "error": print(event.code, event.message) ``` After streaming begins, failures send an `error` event carrying `code` and `message`, then `response.failed`, and close the stream. Responses never sends `data: [DONE]`; it ends with `response.completed` or `response.failed`. OpenAI's Node SDK throws on `error`; Python exposes an event with `type: "error"`. See [Errors](https://docs.sonecentral.com/en/hatalar.md#akis-sirasinda-hata). ## Errors | Status | `code` | When | | --- | --- | --- | | `400` | `missing_input` | Responses request has no input. | | `400` | `invalid_input` | Responses input is not text or an array of items. | | `400` | `invalid_role` | Unknown message role, or a Responses message item without a role. | | `400` | `missing_user_message` | The final message must be a user message containing text. | | `400` | `unsupported_item` | Unknown Responses input item type. | | `400` | `conflicting_parameters` | previous_response_id and conversation were supplied together. Use one. | | `400` | `instructions_too_long` | Responses instructions exceeds 2,000 characters or 4 KB JSON. | | `400` | `invalid_metadata` | Responses metadata exceeds 16 keys, 64 characters per key, 512 characters per string value, or 4 KB JSON. Values must be strings. | | `400` | `safety_identifier_too_long` | Responses safety_identifier exceeds 64 characters. | | `400` | `user_too_long` | Responses user exceeds 64 characters. | | `400` | `previous_response_id_too_long` | Trimmed previous_response_id is longer than a response ID. Send the original response id. | | `400` | `item_id_too_long` | An item_reference ID exceeds 64 characters. | | `400` | `not_cancellable` | Responses complete within the request; there is no background task to cancel. | | `400` | `conversation_model_mismatch` | The conversation belongs to another assistant. Use the same model or start a new conversation. | | `404` | `response_not_found` | Stored response is missing, deleted, older than 30 days or unavailable to the key. On DELETE, also returned when another key stored it. | | `404` | `previous_response_not_found` | The referenced response is missing, deleted, was created with store: false, or belongs to an assistant unavailable to the key. | | `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. | All codes: [Errors](https://docs.sonecentral.com/en/hatalar.md). --- # Vercel AI SDK Page: https://docs.sonecentral.com/en/vercel-ai-sdk Markdown: https://docs.sonecentral.com/en/vercel-ai-sdk.md > Connect useChat directly to /chat with typed rich message parts, or use generateText and streamText through an OpenAI provider on your server. There are two integration paths: - **Direct `useChat`.** Connect your website chat to `POST /chat` with a publishable key, without your own server in between. Product cards, buttons and files arrive as typed data parts. - **As a provider.** Use `generateText` and `streamText` in server code with `@ai-sdk/openai` or `@ai-sdk/openai-compatible`. Examples were tested with `ai` 7, `@ai-sdk/react` 4, `@ai-sdk/openai` 4 and `@ai-sdk/openai-compatible` 3. `/chat` uses the UI message stream format from AI SDK 5 and later. ## Direct `useChat` ```npm npm i ai @ai-sdk/react ``` ```tsx title="components/chat.tsx" 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; const transport = new DefaultChatTransport({ api: 'https://pb.sonecentral.com/api/v1/chat', headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` }, body: { model: 'default' }, }); export function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage, status, error } = useChat({ transport }); const busy = status === 'submitted' || status === 'streaming'; return (
{part.text}
: null, )}{error.message}
: null}{part.text}
; case 'data-sone-staff': return (Ekip: {part.data.text}
); case 'data-sone-catalog': return ({actions.text}
: null} {actions?.buttons?.map((button) => ( {button.title} ))} {actions?.choices?.map((choice) => ( ))}{part.data.text}
); default: return null; } }), )} {activity ?{activity}…
: null}