Threads & Messages
A thread is one conversation with the app. Threads are created implicitly by POST /chat/completions (each response returns its thread_id) and are scoped to the user that started them.
⚠ Requires history access
These endpoints work only when Conversation history over API is enabled in the app's publish settings. When it is off — the recommended state for keys used in browser widgets — they return
403 Forbidden. Chat itself is unaffected.List threads
GET/v1/threads
Query parameters
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| user | string | No | — | The end-user identifier whose threads to list. |
| limit | integer (1–100) | No | 20 | Page size. |
| after | string (UUID) | No | — | Cursor: pass the previous page's last_id to fetch the next page. |
bash
curl "https://cloud.xpectrum.dev/v1/threads?user=user-123&limit=20" \
-H "Authorization: Bearer xpectrum_XXXXXXXXXXXXXXXX"json
{
"object": "list",
"data": [
{
"id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f",
"object": "thread",
"title": "Opening hours",
"created_at": 1754899000,
"updated_at": 1754900000
}
],
"has_more": false,
"limit": 20,
"first_id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f",
"last_id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f"
}Threads are ordered by most recently updated first. While has_more is true, request the next page with after=<last_id>.
Get a thread's messages
GET/v1/threads/{thread_id}/messages
Query parameters
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| user | string | No | — | The end-user identifier the thread belongs to. |
| limit | integer (1–100) | No | 20 | Number of stored exchanges per page. |
| before | string (UUID) | No | — | Cursor: pass the previous page's first_id to walk further back in history. |
bash
curl "https://cloud.xpectrum.dev/v1/threads/5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f/messages?user=user-123" \
-H "Authorization: Bearer xpectrum_XXXXXXXXXXXXXXXX"json
{
"object": "list",
"data": [
{
"id": "a1b2c3d4-...-user",
"object": "message",
"thread_id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f",
"role": "user",
"content": "What are your opening hours?",
"created_at": 1754900000
},
{
"id": "a1b2c3d4-...",
"object": "message",
"thread_id": "5f0c7e0e-6b0a-4f7d-9f1e-8a2b3c4d5e6f",
"role": "assistant",
"content": "We are open 9am-6pm, Monday to Friday.",
"created_at": 1754900000,
"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
}
]
}
],
"has_more": false,
"limit": 20,
"first_id": "a1b2c3d4-...",
"last_id": "e5f6a7b8-..."
}Messages come back oldest first, ready to render as a transcript. Assistant messages may carry citations (the same shape as on chat completions) and, if generation failed, an error string explaining the empty reply. An assistant message id can be passed to GET /messages/{message_id}/suggestions.
⚠ Pagination cursors are exchange ids
first_id / last_id identify stored exchanges (a user + assistant pair), not the individual message ids inside data. Pass first_id back as before — don't use an id ending in -user.With the Xpectrum SDK
typescript
const { data: threads } = await chat.listThreads({ limit: 20 });
const { data: messages } = await chat.getMessages(threads[0].id);
// Continue where the user left off
await chat.send("And what about weekends?", { threadId: threads[0].id });