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.

POST/v1/chat/completions

App modes

Works for AI Chatbot, Autonomous Agent, and Agent Flow apps. For Workflow apps use POST /runs instead.

Request body

PropertyTypeRequiredDefaultDescription
messagesarrayYesOpenAI-style message array. The content of the LAST user message becomes the query; string and multimodal (list-of-parts) content are supported.
streambooleanNofalseStream the answer as server-sent events instead of one JSON response.
userstringNoStable end-user identifier. Threads and memory are scoped to it.
thread_idstring (UUID)NoContinue an existing thread. Omit to start a new one; the id is returned on every response.
variablesobjectNoValues for the agent's input variables, keyed by variable name.
attachmentsarrayNoFiles sent with the message. Each is { "type": "image" | "document" | "audio" | "video", "url": "https://..." } or { "type": ..., "file_id": "<uploaded file id>" }.
channelstringNo"chat"Which channel this message originates from (e.g. chat, whatsapp, voice).
channel_metadataobjectNoFree-form metadata stored with the message for the given channel.
timezonestringNoIANA timezone for the end user, e.g. "Asia/Kolkata" or "America/New_York".

Server-side memory, not client history

Only the last user message is read from 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

Blocking mode works for Chatbot and Agent Flow apps. Autonomous Agent apps reject it with Agent Chat App does not support blocking mode — use a streaming request for those.
bash
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:

json
{
  "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

python
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.

text
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.

python
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:

typescript
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:

json
{
  "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

GET/v1/messages/{message_id}/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.

bash
curl "https://cloud.xpectrum.dev/v1/messages/0b1e0f4a-9a7e-4c2f-b0d5-2f6a8f4f1c11/suggestions?user=user-123" \
  -H "Authorization: Bearer xpectrum_XXXXXXXXXXXXXXXX"
json
{
  "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:

typescript
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

Requires the agent's follow-up suggestions feature to be switched on in the console; otherwise the endpoint returns 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.

json
{
  "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"
}