# Context Assistant HTTP API

## Сессия публичного чата

POST /v1/embed/public-token. Без Authorization. Используется заголовок Origin браузера, не поле тела. Нужен действующий ca_pk_, явно выданный в мастере настройки, и точный разрешённый origin. Пустой список запрещает доступ.

```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"
}
```

При первом запросе пропустите visitorToken; для продления того же гостя передайте значение из ответа. Оно истекает через 30 дней без продления и не принимается как Bearer чата. Для чата, config и context используйте token в Bearer с тем же Origin. SDK управляет кешированием, единым запросом обновления и хранением гостевой сессии. Внешний user ID не передавайте: его генерирует сервер. Публичный доступ не даёт прав управления и Remote Tools. Ошибки: 400 invalid_input; 401 visitor_expired; 403 origin_not_allowed/public_access_denied; 429 rate_limited; 503 not_configured. Бесконечные повторы недопустимы. Отзыв ключа и изменение origins учитываются на последующих запросах.

Выданный визардом ключ синхронизации имеет knowledge:read, knowledge:write и assistants:read в своём рабочем пространстве, без embed:token. Храните CONTEXT_ASSISTANT_SYNC_KEY только в серверной .env; добавьте .env в .gitignore. ca_pk_ не позволяет загружать знания. Существующие секретные ключи и POST /v1/embed/token не меняются.

## HTTP API: авторизация и правила запросов

Базовый адрес REST API: `https://api.contextassistant.io/v1`. Отправляйте JSON с `Content-Type: application/json`. Идентификаторы в примерах нужно заменить своими. В настройках виджета адрес указывается без `/v1`: SDK добавляет этот префикс сам.

| Авторизация | Для чего нужна |
| --- | --- |
| `Authorization: Bearer <server API key>` | Серверные запросы. Ключ хранится в окружении сервера, не в браузере и не в чате с ИИ. Доступ ограничен рабочим пространством ключа. |
| `Authorization: Bearer <embed token>` | Чат в браузере, диалоги и проверка сведений о странице. Временный токен получается через бэкенд вашего сайта. |

| Право серверного ключа | Разрешённые операции |
| --- | --- |
| `embed:token` | Выдача токенов для браузера |
| `assistants:read` | Чтение настроек ассистента и получение UUID по Public ID |
| `knowledge:read` | Списки источников, документов и корзины |
| `knowledge:write` | Создание источников, загрузка, обновление, удаление и восстановление документов. Для импорта используйте отдельный ключ. |

Права ключей не взаимозаменяемы: `embed:token` не позволяет загружать документы. Маршруты `/v1/cabinet/*` используют сессию кабинета и не предназначены для серверной интеграции. `/api/core` и `/api/embed-token` относятся к нашему сайту.

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

Команда использует ключ, уже заданный в окружении сервера. Не вставляйте его значение в промпт или исходники. Запросы записи применяются сразу: предпросмотр и подтверждение реализуются в вашем импорте или интерфейсе. Параметра dry-run у API нет.

## Как получить идентификаторы ассистента

`GET /v1/assistants` -> 200 `{ "items": [...] }`. `GET /v1/assistants/{publicId}` -> 200 с объектом одного ассистента. Оба запроса требуют `assistants:read`; в пути используется Public ID, не 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"
    }
  ]
}
```

| Идентификатор | Где используется |
| --- | --- |
| `publicId` / `asst_...` | assistantId при выдаче токена; точное совпадение при выборе ассистента |
| `id` / UUID | assistantId при создании источника знаний или регистрации подключения бэкенда |
| `sourceId` / UUID | Объединяет документы конкретного источника |
| `externalId` / string | Ваш стабильный идентификатор документа внутри источника |
| `agent` | Необязательный профиль поведения, а не один из перечисленных ID. В обычном подключении его не передавайте. |

## POST /v1/embed/token

Серверный ключ с `embed:token`. JSON-тело до 4 KiB. Обязательный assistantId - Public ID. В origin укажите настроенные схему, домен и порт из разрешённых адресов ассистента. user.externalId берите из проверенной серверной сессии или подписанной cookie конкретного гостя; user.name необязателен.

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

Ответ 200:

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

Ориентируйтесь на expiresAt при обновлении токена. Верните браузеру только эти два поля и `Cache-Control: no-store`. Токен определяет ассистента и посетителя. Разрешённый origin не подтверждает вход пользователя; публичному обработчику токенов нужна защита от злоупотреблений. Не объединяйте гостей под одним ID.

## Диалоги и потоковые ответы

Все маршруты ниже требуют embed-токен. Начните с `POST /v1/conversations`, тело `{ "title": "Delivery question" }` (title необязателен). Ответ 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"
}
```

Отправьте `POST /v1/conversations/{id}/messages`. Обязательное поле content - непустая строка. Необязательный context содержит небольшие структурированные сведения о странице; не весь DOM и не персональные данные. Лимит JSON-тела - 64 KiB. Поле agent пропустите: Public ID в этом поле вызывает `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
```

Сохраните JSON выше в message.json в UTF-8. Успешный ответ - HTTP 200 `text/event-stream`, а не один JSON-документ. Собирайте события SSE независимо от границ сетевых фрагментов. Пример:

```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"}

```

События: token содержит delta; done - итоговый ответ; pending_action - предложенное действие; error - code/error и завершение неудачного потока. В done также могут быть suggestions и references. HTTP 200 сам по себе не подтверждает завершённый ответ. Повторная отправка может создать второе сообщение: после обрыва сначала проверьте историю.

| Метод и путь | Запрос / ответ |
| --- | --- |
| `GET /v1/conversations` | limit/cursor; 200 items с id, title, createdAt, updatedAt и необязательный nextCursor |
| `GET /v1/conversations/{id}` | 200 объект диалога |
| `GET /v1/conversations/{id}/messages` | limit/cursor; 200 items с id, conversationId, role, content, createdAt; возможны toolCalls/usage и nextCursor |
| `POST /v1/conversations/{id}/confirm` | callId, tool, arguments и confirmed из реального предложенного действия; context необязателен. 200 SSE. Сначала предпросмотр и согласие пользователя, для удаления - второе подтверждение. |
| `DELETE /v1/conversations/{id}` | 204 без тела. Удаляет диалог и сообщения безвозвратно; маршрута восстановления нет. |

Для постраничных списков limit по умолчанию равен 20, максимум - 100; неверный/неположительный limit или неверный cursor дают 400. Передавайте nextCursor с URL-кодированием, пока он не исчезнет из ответа. Сообщения идут от старых к новым, диалоги - по последней активности. Перед удалением диалога запросите подтверждение.

## POST /v1/context/validate

Embed-токен. Необязательная предварительная проверка сведений о странице; чат проверяет их повторно. JSON-тело до 16 KiB, очищенный context до 4096 байт, глубина вложенности до 6. Разрешённые поля верхнего уровня: application, workspaceId, page, selection, entity, site. Остальные поля удаляются, строки, похожие на секреты, скрываются. Эти сведения не дают прав доступа и не меняют рабочее пространство из токена.

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

Ответ 200; если данные изменены при очистке, также могут присутствовать массивы droppedFields и redactedPaths:

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

Коды 400: invalid_input при неверном JSON-теле; context_not_an_object, context_too_large, context_too_deep или invalid_context при недопустимом context. Этот запрос не добавляет материалы в базу знаний.

## API базы знаний: создание источника

`POST /v1/knowledge/sources`, серверный ключ с `knowledge:write`. Обязательны assistantId (внутренний UUID) и непустой name. type необязателен, по умолчанию manual; это только метка, она не запускает обход сайта или расписание. Лимит тела - 256 KiB.

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

Ответ 201. Сохраните id как sourceId и его связь с ассистентом в своих настройках:

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

`GET /v1/knowledge/sources` требует `knowledge:read` и возвращает 200 `{ "items": [...] }` для рабочего пространства, без пагинации. Объекты источников не содержат assistantId. Не определяйте владельца по имени и не создавайте новый источник при каждом импорте.

## POST /v1/knowledge/documents

Серверный ключ с `knowledge:write`. Создание или обновление одного документа:

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

| Поле | Контракт |
| --- | --- |
| `sourceId` | Обязательный UUID существующего источника в этом рабочем пространстве |
| `externalId` | Необязательная строка до 512 байт UTF-8. Для импорта и синхронизации всегда задавайте её. |
| `title` | Необязательная строка, пробелы по краям удаляются |
| `card` | Необязательный объект карточки: title (1–120 символов, по умолчанию заголовок документа), url (HTTP/S), image (HTTPS), subtitle (до 100 символов), badges (до 4 строк по 48 символов). Ссылки до 2048 байт UTF-8, без логина и пароля. Без image карточка текстовая, без url - её нельзя открыть нажатием. При повторной загрузке отсутствие card сохраняет карточку; card: null явно удаляет её. Объект card заменяет все поля карточки. |
| `contentType` | Обязательное: text, markdown, html или json (не MIME-тип) |
| `content` | Обязательная непустая строка с самим содержимым, в том числе для JSON. Всё 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
```

Сохраните пример в document.json в UTF-8. Новый документ возвращает 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"
}
```

Идентичность документа - пара (sourceId, externalId). Изменения возвращают 200 updated с тем же id; одинаковый текст и метаданные - unchanged. Изменение только title/card обновляет метаданные без повторной векторизации готового документа. Изменение content ставит документ на индексацию. Без externalId каждый POST создаёт новый документ. PATCH для документа нет.

```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": [
      "Светлый дуб",
      "Серая ткань"
    ]
  }
}
```

Карточки выбираются из результатов базы знаний без Remote Tools. Сервер передаёт сохранённые поля в references ответа чата. Изображения задаются ссылками: файлы не загружаются и не анализируются. Обновляйте цены и наличие синхронизацией: карточки из базы знаний не проверяют склад в реальном времени. Сейчас карточки видны в текущем ответе; при повторной загрузке истории восстанавливается текст, но не карточки.

syncStatus описывает результат записи, а не готовность индекса. status принимает pending, processing, ready или failed. URL в content не скачивается. PDF и изображения перед загрузкой преобразуйте в проверенный текст. Загружайте только материалы, доступные аудитории ассистента: источник не задаёт индивидуальные права посетителей.

## POST /v1/knowledge/documents/bulk

В текущем контракте API есть пакетная загрузка с `knowledge:write`: 1-100 документов, externalId обязателен у каждого, JSON-тело до 8 MiB, content каждого документа до 256 KiB. Повторяющиеся пары (sourceId, externalId) внутри пакета отклоняются. Проверьте доступность маршрута в своём окружении; если bulk недоступен, используйте одиночные запросы.

```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, по одному результату на документ в порядке запроса:

```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
}
```

Поля и источники проверяются до записи, но пакет не является одной транзакцией БД. При ошибке хранилища или очереди часть документов может уже сохраниться. Повторяйте запрос с теми же стабильными идентификаторами, не генерируйте новые externalId. Индексация остаётся асинхронной.

## Проверка индексации и постраничная загрузка

`GET /v1/knowledge/documents?limit=100` требует `knowledge:read`. В ответе 200 items содержат id, sourceId, необязательный externalId, title, необязательный card, contentType, status, необязательный error и createdAt. Исходный content, checksum и syncStatus в списке не возвращаются.

```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 по умолчанию 20, максимум 100; неверные/неположительные значения и неверные курсоры возвращают 400. Передавайте nextCursor с URL-кодированием, пока он не исчезнет. Список относится ко всему рабочему пространству, не к одному ассистенту или источнику. Фильтруйте sourceId в импорте. Фильтра sourceId в query и GET /documents/{id} нет; нужный документ ищется по id в постраничном списке.

Опрашивайте с паузами и предельным временем ожидания. ready означает, что документ проиндексирован; реальные ответы всё равно нужно проверить. При failed разберите error без вывода закрытого содержимого. Повторная отправка неизменного failed-документа не запускает индексацию заново. Сохраняйте исходные материалы у себя и сообщайте о сбое вместо бесконечных повторов.

## Обновления, корзина и безопасная синхронизация

| Метод и путь | Право и ответ |
| --- | --- |
| `DELETE /v1/knowledge/documents/{documentId}` | knowledge:write; 204, переносит в корзину и убирает фрагменты из индекса |
| `GET /v1/knowledge/documents/trash` | knowledge:read; 200 {items: [...]}, последние 100, без курсора |
| `POST /v1/knowledge/documents/{documentId}/restore` | knowledge:write; тело не требуется; 200 документ со status pending; снова ставит на индексацию |
| `DELETE /v1/knowledge/documents/{documentId}/purge` | knowledge:write; 204; безвозвратно удаляет документ из корзины |
| `DELETE /v1/knowledge/sources/{sourceId}` | knowledge:write; 204; безвозвратно удаляет источник и все его документы, включая корзину |

Восстановление может вернуть 409 conflict, если активный документ уже занимает пару (sourceId, externalId). Не удаляйте ни одну версию автоматически. Маршруты удаления не реализуют ваш процесс согласования: сначала покажите предпросмотр и получите отдельное двойное подтверждение. DELETE источника и purge не подходят для обычной очистки при синхронизации.

Стройте синхронизацию вокруг постоянного sourceId и стабильных externalId. Сравнивайте кандидатов на удаление только внутри этого источника, после полной успешной выгрузки и чтения всех страниц списка документов. Недоступность источника, пустая частичная выгрузка или пропущенная страница не должны приводить к массовому удалению. По умолчанию используйте dry-run, сохраняйте исходный контент и журнал запусков, останавливайтесь при необычно большом числе удалений. Расписание, webhook и чтение источника реализуются в проекте клиента; этот API не создаёт обходчик сайтов.

## Ошибки и ограничения запросов

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

| Статус / код | Причина и действие |
| --- | --- |
| 400 `invalid_input` | Неверные поля, формат, курсор или размер тела; исправьте запрос, а не повторяйте его без изменений |
| 400 `invalid_context` | Недопустимые сведения о странице |
| 401 `unauthorized` | Нет ключа, ключ отозван, embed-токен неверный или истёк |
| 403 `forbidden` | У ключа нет требуемого права |
| 403 `origin_not_allowed` | Заявленный origin не входит в разрешённые адреса ассистента |
| 404 `not_found`, `assistant_not_found`, `source_not_found`, `document_not_found`, `conversation_not_found` | Объект отсутствует или не входит в область доступа. API управления ассистентами возвращает not_found, выдача токенов - assistant_not_found. |
| 409 `conflict` | Восстанавливаемый документ конфликтует с активной версией |
| 429 | Лимит частоты или квота. Учитывайте Retry-After, если он есть; ограничивайте число повторов. |
| 5xx | Сбой сервиса, хранилища или провайдера. Документ мог сохраниться: повторяйте загрузку со стабильным externalId. Не пересылайте сообщения чата вслепую. |

Лимиты тела: токен 4 KiB; чат 64 KiB; одиночный запрос знаний 256 KiB; bulk 8 MiB и до 256 KiB content на элемент. Прокси может ограничивать размер сильнее и возвращать не-JSON ошибки, например 413. Проверяйте HTTP-статус и тип ответа перед разбором. Для POST с SSE в браузере используйте fetch с Authorization, а не EventSource.
