Chat Completions
Send a message to an AI Chatbot, Autonomous Agent, or Agent Flow app and get the reply — blocking or streamed. Drop-in compatible with OpenAI clients.
ℹ App modes
POST /runs instead.Request body
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| messages | array | Yes | — | OpenAI-style message array. The content of the LAST user message becomes the query; string and multimodal (list-of-parts) content are supported. |
| stream | boolean | No | false | Stream the answer as server-sent events instead of one JSON response. |
| user | string | No | — | Stable end-user identifier. Threads and memory are scoped to it. |
| thread_id | string (UUID) | No | — | Continue an existing thread. Omit to start a new one; the id is returned on every response. |
| variables | object | No | — | Values for the agent's input variables, keyed by variable name. |
| attachments | array | No | — | Files sent with the message. Each is { "type": "image" | "document" | "audio" | "video", "url": "https://..." } or { "type": ..., "file_id": "<uploaded file id>" }. |
| channel | string | No | "chat" | Which channel this message originates from (e.g. chat, whatsapp, voice). |
| channel_metadata | object | No | — | Free-form metadata stored with the message for the given channel. |
| timezone | string | No | — | IANA timezone for the end user, e.g. "Asia/Kolkata" or "America/New_York". |
⚠ Server-side memory, not client history
messages — earlier entries are ignored. Multi-turn context comes from passing thread_id back on the next request, not from resending the transcript.Blocking request
⚠ Not for Autonomous Agent apps
Agent Chat App does not support blocking mode — use a streaming request for those.curl https://cloud.xpectrum.dev/v1/chat/completions \
-H "Authorization: Bearer xpectrum_XXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{
"messages": [{ "role": "user", "content": "What are your opening hours?" }],
"user": "user-123"
}'Response — the OpenAI shape plus Xpectrum extension fields:
{
"id": "chatcmpl-0b1e0f4a-9a7e-4c2f-b0d5-2f6a8f4f1c11",
"object": "chat.completion",
"created": 1754900000,
"model": "e040c4d0-9b1e-4718-aaf9-eeb2d450b467",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "We are open 9am-6pm, Monday to Friday." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 412, "completion_tokens": 28, "total_tokens": 440 },
"thread_id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f",
"run_id": "d3adbeef-0000-4000-8000-000000000000",
"mode": "chatbot",
"citations": [
{
"object": "citation",
"knowledge_id": "3bffeb63-...",
"knowledge_name": "Help center",
"document_id": "9c1d...",
"document_name": "opening-hours.md",
"chunk_id": "b7e2...",
"content": "We are open 9am-6pm Monday to Friday.",
"score": 0.91,
"position": 1
}
]
}thread_id, run_id, mode (chatbot, agent or flow) and citations (present when the agent has knowledge retrieval enabled) are Xpectrum extensions — standard OpenAI clients simply ignore them. The model value is the agent id (see Models).
Multi-turn conversations
first = client.chat.completions.create(
model="my-app",
messages=[{"role": "user", "content": "My name is Ada."}],
extra_body={"user": "user-123"},
)
followup = client.chat.completions.create(
model="my-app",
messages=[{"role": "user", "content": "What is my name?"}],
extra_body={
"user": "user-123",
"thread_id": first.model_extra["thread_id"],
},
) # -> "Your name is Ada."Streaming
With "stream": true the response is text/event-stream: OpenAI-style chat.completion.chunk events terminated by data: [DONE]. Keep-alive pings arrive as SSE comments (: ping), which clients ignore.
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"e040...","thread_id":"5f0c...","run_id":"d3ad...","choices":[{"index":0,"delta":{"role":"assistant","content":"We"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"e040...","thread_id":"5f0c...","run_id":"d3ad...","choices":[{"index":0,"delta":{"content":" are open"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"e040...","thread_id":"5f0c...","run_id":"d3ad...","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":412,"completion_tokens":28,"total_tokens":440},"citations":[ ... ]}
data: [DONE]Every chunk carries thread_id and run_id — save the run id if you want to be able to POST /runs/{run_id}/cancel the generation. The final chunk also carries usage and, when available, citations.
stream = client.chat.completions.create(
model="my-app",
messages=[{"role": "user", "content": "Tell me a story."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)Using the Xpectrum SDK
xpectrum wraps this endpoint — send() resolves with the finished reply and stream() delivers tokens as they arrive, with SSE parsing, errors, and aborts handled for you:
import { XpectrumChat } from "xpectrum";
const chat = new XpectrumChat({
baseUrl: "https://cloud.xpectrum.dev/v1",
apiKey: "xpectrum_XXXXXXXXXXXXXXXX",
user: "user-123",
});
// Whole reply at once
const first = await chat.send("My name is Ada.");
console.log(first.content);
// Multi-turn: pass the threadId back
const followup = await chat.send("What is my name?", {
threadId: first.threadId,
}); // -> "Your name is Ada."
// Token by token
await chat.stream("Tell me a story.", {
threadId: first.threadId,
onToken: (delta, full) => render(full),
onDone: (result) => console.log("usage:", result.usage),
});Results expose the same extension fields as the raw API — threadId, runId, messageId, usage, and citations — and options accept variables, attachments, channel, and timezone.
Attachments and variables
Send files alongside the message with attachments — by public URL, or by the file_id of a previous upload — and fill the agent's input variables with variables:
{
"messages": [{ "role": "user", "content": "Summarise this report." }],
"attachments": [
{ "type": "document", "url": "https://example.com/report.pdf" },
{ "type": "image", "file_id": "b4f1c2d3-..." }
],
"variables": { "language": "en" },
"user": "user-123"
}Follow-up suggestions
Questions the user is likely to ask next, generated from the thread so far. message_id is the assistant message to follow on from — the completion id with the chatcmpl- prefix removed, or an assistant message id from GET /threads/{thread_id}/messages. It costs one model call, so request it only when you are about to show the suggestions.
curl "https://cloud.xpectrum.dev/v1/messages/0b1e0f4a-9a7e-4c2f-b0d5-2f6a8f4f1c11/suggestions?user=user-123" \
-H "Authorization: Bearer xpectrum_XXXXXXXXXXXXXXXX"{
"object": "list",
"message_id": "0b1e0f4a-9a7e-4c2f-b0d5-2f6a8f4f1c11",
"data": [
"Are you open on public holidays?",
"Can I book an appointment outside these hours?"
]
}With the SDK:
const reply = await chat.send("What are your opening hours?");
const questions = await chat.getSuggestions(reply.messageId!);
// -> ["Are you open on public holidays?", ...]⚠ Must be enabled on the agent
403 with code suggestions_disabled.Images (vision)
OpenAI-style image_url parts in the last user message are forwarded to the app's file pipeline. Both remote https:// URLs and base64 data: URIs work (the agent must have file upload / vision enabled). This is equivalent to an attachments entry of type image.
{
"messages": [
{
"role": "user",
"content": [
{ "type": "text", "text": "What is in this picture?" },
{ "type": "image_url", "image_url": { "url": "https://example.com/photo.jpg" } }
]
}
],
"user": "user-123"
}