# Context Assistant HTTP API

## Public chat session

POST /v1/embed/public-token. No Authorization header. Browser sends its Origin header; the server never trusts a body origin. Requires an active ca_pk_ key explicitly enabled in assistant setup and an exact allowed origin. An empty allowlist denies access.

```json
{
  "publicKey": "YOUR_PUBLIC_KEY",
  "visitorToken": "OPTIONAL_PREVIOUS_VISITOR_TOKEN"
}
```

200, Cache-Control: no-store:

```json
{
  "token": "SHORT_LIVED_ACCESS_JWT",
  "expiresAt": "2026-09-14T12:15:00Z",
  "visitorToken": "SIGNED_GUEST_RENEWAL_CREDENTIAL"
}
```

Omit visitorToken on first use; send the returned value to renew the same guest. It expires after 30 days without renewal and cannot be used as a chat bearer token. Use token as Bearer for chat/config/context requests, with the same Origin. The SDK handles caching, single-flight renewal and guest persistence. Never send an external user ID: the server generates it. Public access has no management or Remote Tools rights. 400 invalid_input; 401 visitor_expired; 403 origin_not_allowed/public_access_denied; 429 rate_limited; 503 not_configured. No endless retries. Revocation and origin changes take effect on subsequent requests.

Wizard-issued sync keys grant knowledge:read, knowledge:write and assistants:read in their workspace, not embed:token. Store CONTEXT_ASSISTANT_SYNC_KEY only in server .env; ignore .env in git. Public ca_pk_ keys never authorize knowledge uploads. Existing secret keys and POST /v1/embed/token are unchanged.

## HTTP API: authentication and conventions

REST base URL: `https://api.contextassistant.io/v1`. Send JSON with `Content-Type: application/json`. Examples use placeholder identities: replace them with your own. Widget options use the origin without `/v1`; the SDK adds the prefix.

| Credential | Where it is used |
| --- | --- |
| `Authorization: Bearer <server API key>` | Server-to-server calls. Keep the key in server secrets, never in a browser or AI chat. Access is scoped to the key's workspace. |
| `Authorization: Bearer <embed token>` | Browser chat, conversations and context validation. The public SDK obtains it directly from CA; private integrations use their own backend. |

| Server-key scope | Allowed operations |
| --- | --- |
| `embed:token` | Issue browser tokens |
| `assistants:read` | Read assistant settings and resolve a Public ID to an internal UUID |
| `knowledge:read` | List sources, documents and trash |
| `knowledge:write` | Create sources, upload/update documents, delete and restore. Use a separate import key. |

API-key scopes are not interchangeable. A key with only `embed:token` cannot upload documents. `/v1/cabinet/*` uses the account session and is not the integration API. `/api/core` and `/api/embed-token` belong to our own website.

```bash
curl --fail-with-body "https://api.contextassistant.io/v1/assistants" \
  -H "Authorization: Bearer ${CONTEXT_ASSISTANT_SYNC_KEY}"
```

The command reads a key already set in the server environment. Do not paste its value into a prompt or save it in source code. Management writes take effect immediately: preview/approval belongs in your importer or UI, not an imaginary API dry-run flag.

## Find the assistant and its identifiers

`GET /v1/assistants` -> 200 `{ "items": [...] }`. `GET /v1/assistants/{publicId}` -> 200 with one assistant object. Both require `assistants:read`; the path uses Public ID, not UUID.

```json
{
  "items": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "workspaceId": "44444444-4444-4444-8444-444444444444",
      "publicId": "asst_example",
      "name": "Store assistant",
      "profile": "general",
      "instructions": "",
      "modelConfig": {},
      "toolPolicy": {},
      "allowedOrigins": [
        "https://shop.example"
      ],
      "createdAt": "2026-09-10T12:00:00Z",
      "updatedAt": "2026-09-10T12:00:00Z"
    }
  ]
}
```

| Identifier | Use |
| --- | --- |
| `publicId` / `asst_...` | Token request assistantId; exact match when selecting an assistant |
| `id` / UUID | assistantId when creating a knowledge source or registering a backend integration |
| `sourceId` / UUID | Groups documents imported for a particular source |
| `externalId` / string | Your stable document ID inside that source |
| `agent` | Optional behavior profile, not any of the IDs above. Omit it in a basic integration. |

## POST /v1/embed/token

Server key with `embed:token`. JSON body up to 4 KiB. Required assistantId is the Public ID. Set origin to the configured scheme, host and port from the assistant allowlist. Derive user.externalId from a verified server session or a signed per-guest cookie; user.name is optional.

```json
{
  "assistantId": "asst_example",
  "origin": "https://shop.example",
  "user": {
    "externalId": "member_42",
    "name": "Alex"
  }
}
```

200 response:

```json
{
  "token": "<temporary JWT>",
  "expiresAt": "2026-09-10T12:15:00Z"
}
```

Use expiresAt to schedule renewal. Return only these two fields to the browser with `Cache-Control: no-store`. The token selects the assistant and visitor. An allowed origin is not proof of user login; a public token endpoint still needs abuse protection. Keep guests separate.

## Conversations and streaming replies

Every route below requires an embed token. Start with `POST /v1/conversations`, body `{ "title": "Delivery question" }` (title is optional). Response 201:

```json
{
  "id": "55555555-5555-4555-8555-555555555555",
  "assistantId": "asst_example",
  "title": "Delivery question",
  "createdAt": "2026-09-10T12:00:00Z",
  "updatedAt": "2026-09-10T12:00:00Z"
}
```

Send `POST /v1/conversations/{id}/messages`. Required content is a non-empty string. Optional context contains small structured page facts; no full DOM or personal data. The JSON body limit is 64 KiB. Omit agent; an assistant Public ID here causes `unknown agent profile`.

```json
{
  "content": "How long does delivery take?",
  "context": {
    "page": {
      "type": "product",
      "id": 184
    }
  }
}
```

```bash
curl -N --fail-with-body "https://api.contextassistant.io/v1/conversations/${CONVERSATION_ID}/messages" \
  -H "Authorization: Bearer ${EMBED_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary @message.json
```

Save the JSON above as UTF-8 message.json. A successful response is HTTP 200 `text/event-stream`, not one JSON document. Parse SSE frames across arbitrary network chunks. Example:

```text
event: token
data: {"delta":"Delivery takes 2-4 business days."}

event: done
data: {"conversationId":"55555555-5555-4555-8555-555555555555","userMessageId":"user-message-uuid","messageId":"reply-message-uuid","message":"Delivery takes 2-4 business days.","model":"model-id","agent":"general","title":"Delivery question"}

```

Events: token carries delta; done carries the final message; pending_action carries a proposed action; error carries code/error and terminates a failed stream. done may also contain suggestions and references. HTTP 200 alone is not a completed reply. A retry may create a second message: inspect history after an interrupted send.

| Method and path | Request / response |
| --- | --- |
| `GET /v1/conversations` | limit/cursor; 200 items of id, title, createdAt, updatedAt and optional nextCursor |
| `GET /v1/conversations/{id}` | 200 one conversation object |
| `GET /v1/conversations/{id}/messages` | limit/cursor; 200 items of id, conversationId, role, content, createdAt; optional toolCalls/usage and nextCursor |
| `POST /v1/conversations/{id}/confirm` | callId, tool, arguments and confirmed from the actual pending action; optional context. 200 SSE. Preview and user approval first; deletion needs a second confirmation. |
| `DELETE /v1/conversations/{id}` | 204 without a body. Permanently erases the conversation and messages; no restore endpoint. |

For cursor lists, limit defaults to 20 and is capped at 100; a non-positive/invalid limit or invalid cursor gives 400. Follow nextCursor with URL encoding until omitted. Messages are listed oldest first; conversation lists are ordered by recent activity. Ask for confirmation before deleting a conversation.

## POST /v1/context/validate

Embed token. Optional preflight for page facts; chat validates them again. JSON body up to 16 KiB, sanitized context up to 4096 bytes, maximum nesting depth 6. Allowed top-level fields: application, workspaceId, page, selection, entity, site. Other fields are dropped and secret-looking strings are redacted. These facts do not grant access or change the authenticated workspace.

```json
{
  "context": {
    "page": {
      "type": "product",
      "id": 184
    }
  }
}
```

200 response, with optional droppedFields and redactedPaths arrays when sanitization changed anything:

```json
{
  "context": {
    "page": {
      "type": "product",
      "id": 184
    }
  }
}
```

400 codes: invalid_input for an invalid JSON body; context_not_an_object, context_too_large, context_too_deep or invalid_context for invalid context. This endpoint does not add material to the knowledge base.

## Knowledge API: create a source

`POST /v1/knowledge/sources`, server key with `knowledge:write`. Required: assistantId (internal UUID) and non-empty name. type is optional, defaults to manual and is only a label; it does not start a crawler or scheduler. Body limit: 256 KiB.

```json
{
  "assistantId": "11111111-1111-4111-8111-111111111111",
  "name": "Store help",
  "type": "manual"
}
```

201 response. Save id as sourceId and retain its assistant association in your own settings:

```json
{
  "id": "22222222-2222-4222-8222-222222222222",
  "name": "Store help",
  "type": "manual",
  "createdAt": "2026-09-10T12:00:00Z"
}
```

`GET /v1/knowledge/sources` requires `knowledge:read` and returns 200 `{ "items": [...] }` for the workspace, without pagination. Source objects do not expose assistantId. Do not infer ownership by name or create a new source on every import.

## POST /v1/knowledge/documents

Server key with `knowledge:write`. Create or update one document:

```json
{
  "sourceId": "22222222-2222-4222-8222-222222222222",
  "externalId": "help:delivery:en",
  "title": "Delivery",
  "contentType": "markdown",
  "content": "# Delivery\nDelivery takes 2-4 business days."
}
```

| Field | Contract |
| --- | --- |
| `sourceId` | Required, existing source UUID in this workspace |
| `externalId` | Optional string, up to 512 UTF-8 bytes. Always set it for imports and sync. |
| `title` | Optional string, trimmed |
| `card` | Optional presentation object: title (1–120 characters, defaults to document title), url (HTTP/S), image (HTTPS), subtitle (up to 100 characters), badges (up to 4 strings, 48 characters each). URLs up to 2048 UTF-8 bytes, without credentials. Omit image for a text card; omit url for a non-clickable card. Omit card to preserve existing presentation metadata on upsert; send card: null to explicitly remove it. A card object replaces all card fields. |
| `contentType` | Required: text, markdown, html or json (not a MIME type) |
| `content` | Required non-empty string containing the actual material, even for JSON. Entire request JSON <= 256 KiB. |

```bash
curl --fail-with-body "https://api.contextassistant.io/v1/knowledge/documents" \
  -H "Authorization: Bearer ${CONTEXT_ASSISTANT_SYNC_KEY}" \
  -H "Content-Type: application/json" \
  --data-binary @document.json
```

Save the example as UTF-8 document.json. New documents return 201:

```json
{
  "id": "33333333-3333-4333-8333-333333333333",
  "sourceId": "22222222-2222-4222-8222-222222222222",
  "externalId": "help:delivery:en",
  "title": "Delivery",
  "contentType": "markdown",
  "status": "pending",
  "createdAt": "2026-09-10T12:00:00Z",
  "syncStatus": "created"
}
```

Identity is (sourceId, externalId). Changes return 200 updated with the same id; identical content and metadata return unchanged. Title/card-only changes update metadata without re-embedding ready documents. Content changes queue indexing. Without externalId every POST creates a new document. There is no PATCH document endpoint.

```json
{
  "sourceId": "22222222-2222-4222-8222-222222222222",
  "externalId": "product:42:ru",
  "title": "Кресло Oslo",
  "contentType": "text",
  "content": "Кресло Oslo: светлый дуб, серая ткань. Цена 120 EUR.",
  "card": {
    "title": "Кресло Oslo",
    "url": "https://shop.example/products/42",
    "image": "https://shop.example/images/42.jpg",
    "subtitle": "120 EUR",
    "badges": [
      "Светлый дуб",
      "Серая ткань"
    ]
  }
}
```

Cards can be selected from knowledge results without Remote Tools. The server supplies stored fields through references in the chat response. Images are linked, not uploaded or analysed. Keep prices and availability current through synchronization; knowledge cards are not a live stock check. Cards currently appear in the live reply; reloading the conversation restores text, not cards.

syncStatus describes the write outcome, not indexing readiness. status is pending, processing, ready or failed. A URL in content is not fetched. Convert PDFs/images to reviewed text before upload. Upload only material the assistant's audience may access; a source is not a per-visitor access-control list.

## POST /v1/knowledge/documents/bulk

The current API contract includes bulk upsert with `knowledge:write`: 1-100 documents, required externalId per item, 8 MiB maximum JSON body, and 256 KiB maximum content per item. Duplicate (sourceId, externalId) inside a batch is rejected. Check route availability in your environment; use the single-document route if bulk is unavailable.

```json
{
  "documents": [
    {
      "sourceId": "22222222-2222-4222-8222-222222222222",
      "externalId": "help:delivery:en",
      "title": "Delivery",
      "contentType": "markdown",
      "content": "# Delivery\nDelivery takes 2-4 business days."
    }
  ]
}
```

200 response, one item per input in the same order:

```json
{
  "items": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "sourceId": "22222222-2222-4222-8222-222222222222",
      "externalId": "help:delivery:en",
      "title": "Delivery",
      "contentType": "markdown",
      "status": "pending",
      "createdAt": "2026-09-10T12:00:00Z",
      "syncStatus": "created"
    }
  ],
  "created": 1,
  "updated": 0,
  "unchanged": 0
}
```

All input and source references are checked before writes, but the batch is not one database transaction. A storage/queue failure may leave earlier items applied. Retry with the same stable identities; do not generate new externalId values. Indexing is still asynchronous.

## Check indexing status and paginate

`GET /v1/knowledge/documents?limit=100` requires `knowledge:read`. 200 items contain id, sourceId, optional externalId, title, optional card, contentType, status, optional error and createdAt. Raw content, checksum and syncStatus are not returned in this list.

```json
{
  "items": [
    {
      "id": "33333333-3333-4333-8333-333333333333",
      "sourceId": "22222222-2222-4222-8222-222222222222",
      "externalId": "help:delivery:en",
      "title": "Delivery",
      "contentType": "markdown",
      "status": "ready",
      "createdAt": "2026-09-10T12:00:00Z"
    }
  ],
  "nextCursor": "<opaque cursor; omitted on last page>"
}
```

limit defaults to 20, caps at 100; invalid/non-positive values and invalid cursors return 400. Follow nextCursor, URL-encoded, until absent. Listings cover the workspace, not one assistant or source. Filter sourceId in your importer. There is no sourceId query filter or GET /documents/{id}; find the document by id in the paginated list.

Poll with pauses and a deadline. ready means the document is indexed; still test actual answers. On failed inspect error without logging private content. An unchanged repost of a failed document does not automatically retry indexing. Keep the original material locally and report the failure instead of an infinite retry loop.

## Updates, trash and safe synchronization

| Method and path | Scope and response |
| --- | --- |
| `DELETE /v1/knowledge/documents/{documentId}` | knowledge:write; 204, moves to trash and removes indexed chunks |
| `GET /v1/knowledge/documents/trash` | knowledge:read; 200 {items: [...]}, latest 100, no cursor |
| `POST /v1/knowledge/documents/{documentId}/restore` | knowledge:write; no request body needed; 200 document with status pending; queues indexing again |
| `DELETE /v1/knowledge/documents/{documentId}/purge` | knowledge:write; 204; permanently deletes a document already in trash |
| `DELETE /v1/knowledge/sources/{sourceId}` | knowledge:write; 204; irreversibly deletes the source and all its documents, including trash |

Restore may return 409 conflict if an active document already owns the same (sourceId, externalId). Do not purge either version automatically. Deletion endpoints do not implement your approval workflow: show a preview and obtain separate double confirmation before a destructive operation. Never use source DELETE or purge as routine sync cleanup.

Plan sync around a persistent sourceId and stable externalId values. Compare deletion candidates only within that source, after a complete successful source export and all document pages. A source outage, empty partial export or missing list page must never trigger mass deletion. Use dry-run by default, retain original content and a run log, and stop at an unusual deletion count. Scheduling, webhooks and source reading run in the customer project; this API does not create a crawler.

## Errors and request limits

```json
{
  "code": "forbidden",
  "error": "the API key lacks the required scope"
}
```

| Status / code | Meaning and action |
| --- | --- |
| 400 `invalid_input` | Invalid fields, format, cursor or body size; correct the request rather than retrying it unchanged |
| 400 `invalid_context` | Invalid structured page facts |
| 401 `unauthorized` | Missing/revoked key or invalid/expired embed token |
| 403 `forbidden` | Key lacks the required scope |
| 403 `origin_not_allowed` | Declared origin is not in the assistant allowlist |
| 404 `not_found`, `assistant_not_found`, `source_not_found`, `document_not_found`, `conversation_not_found` | Object absent or outside the authorized scope. Assistant management uses not_found; token issuance uses assistant_not_found. |
| 409 `conflict` | A restored document conflicts with an active identity |
| 429 | Rate or quota limit. Honor Retry-After when present; use bounded backoff. |
| 5xx | Service/storage/provider failure. A document write may already have happened: retry uploads with stable externalId. Do not blindly resend chat messages. |

Body limits: token 4 KiB; chat 64 KiB; one knowledge request 256 KiB; bulk 8 MiB plus 256 KiB content per item. Reverse proxies can impose smaller limits and return non-JSON errors such as 413. Check HTTP status and content type before parsing. Browser requests use fetch (with Authorization), not EventSource, for POST-based SSE.
