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

PropertyTypeRequiredDefaultDescription
userstringNoThe end-user identifier whose threads to list.
limitinteger (1–100)No20Page size.
afterstring (UUID)NoCursor: 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

PropertyTypeRequiredDefaultDescription
userstringNoThe end-user identifier the thread belongs to.
limitinteger (1–100)No20Number of stored exchanges per page.
beforestring (UUID)NoCursor: 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 });