
## Глубокий поиск (research)

`POST /v1/research`

Запускает глубокий итеративный поиск с многошаговым агентным циклом «искать → читать → анализировать». Возвращает структурированный отчёт с разделами, источниками и опциональными уточняющими вопросами. Эндпоинт всегда работает в потоковом режиме через Server-Sent Events — один запрос длится 10–60 секунд и не помещается в типичный таймаут синхронного HTTP.

## Поля запроса (body)

| Поле | Тип | Обяз. | По умолч. | Описание |
|------|-----|:-----:|-----------|---------|
| `query` | string | да | — | Текст исследовательского запроса. От 1 до 2000 символов |
| `provider` | string | нет | research-движок по умолчанию инстанса | Провайдер с поддержкой research: `tavily`, `exa`, `you-com`, `linkup`, `perplexity` или `jina`. Платформенный `bitrix-search` поддерживает research в зависимости от инстанса. Актуальный список — в [`GET /v1/search/providers`](/docs/search/providers) с `capabilities.modes.research: true` |
| `maxSteps` | number | нет | 5 | Максимум шагов агентного цикла. От 1 до 10. Адаптер ограничивает значение, если внешний провайдер не принимает столько шагов |
| `lang` | string | нет | `auto` | Язык исследования: `ru`, `en` или `auto` |
| `includeDomains` | string[] | нет | `[]` | Поиск только по доменам из списка. До 50 имён хоста. Отдельные движки берут не больше 20 — при превышении список обрезается, а в кадре `done` появляется `ignored_filters` с `includeDomains_truncated` |
| `excludeDomains` | string[] | нет | `[]` | Исключить домены. До 50 имён хоста. Отдельные движки берут не больше 20 — при превышении список обрезается, а в кадре `done` появляется `ignored_filters` с `excludeDomains_truncated` |

## Примеры

### curl — личный ключ

```bash
curl -N -X POST https://vibecode.bitrix24.tech/v1/research \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Сравни возможности Cursor 3 и Windsurf для AI-кодинга",
    "provider": "tavily",
    "maxSteps": 5,
    "lang": "ru"
  }'
```

### curl — OAuth-приложение

```bash
curl -N -X POST https://vibecode.bitrix24.tech/v1/research \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Сравни возможности Cursor 3 и Windsurf для AI-кодинга",
    "provider": "tavily",
    "maxSteps": 5,
    "lang": "ru"
  }'
```

### JavaScript — личный ключ

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/research', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Сравни возможности Cursor 3 и Windsurf для AI-кодинга',
    provider: 'tavily',
    maxSteps: 5,
    lang: 'ru',
  }),
})

const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })
  const events = buffer.split('\n\n')
  buffer = events.pop() ?? ''
  for (const block of events) {
    const eventLine = block.split('\n').find(l => l.startsWith('event: '))
    const dataLine = block.split('\n').find(l => l.startsWith('data: '))
    if (!eventLine || !dataLine) continue
    const event = eventLine.slice(7)
    const data = JSON.parse(dataLine.slice(6))
    if (event === 'done') {
      console.log('Готово:', data.synthesized_answer.slice(0, 200))
      console.log('Источников:', data.sources.length)
    }
  }
}
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/research', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Сравни возможности Cursor 3 и Windsurf для AI-кодинга',
    provider: 'tavily',
    maxSteps: 5,
    lang: 'ru',
  }),
})

const reader = res.body.getReader()
```

## Поток событий (SSE)

Ответ приходит как поток `text/event-stream`. Каждое событие — пара `event: <type>` + `data: <JSON>`, разделённая пустой строкой. Поток гарантированно завершается одним из двух событий: `done` (успех) или `error` (сбой).

| Событие | Когда возникает | Поля `data` |
|---------|-----------------|-------------|
| `start` | В начале запроса | `research_id`, `provider` |
| `thinking` | Промежуточные размышления агента | `content` — текстовый фрагмент |
| `tool_call` | Агент вызвал внутренний инструмент | `tool` (`web_search` / `content_extraction` / `external_tool`), `description` |
| `tool_result` | Инструмент вернул результат | `description`, `items_count` |
| `section_complete` | Завершён раздел исследования с привязанными источниками | `section.title`, `section.content`, `section.sourceIds` |
| `answer_delta` | Очередной фрагмент финального ответа | `content` — кусок текста |
| `done` | Финальный блок | см. таблицу «Поля события `done`» ниже |
| `error` | Сбой во время обработки | `error.code`, `error.message` |

Какие именно промежуточные события придут — зависит от провайдера. Ни одно из них не обязательно: получение `start` и `done` достаточно для штатного завершения.

## Поля события `done`

| Поле | Тип | Описание |
|------|-----|---------|
| `query` | string | Текст исходного запроса |
| `provider` | string | Идентификатор провайдера, который обработал запрос (например, `tavily`, `exa`) |
| `synthesized_answer` | string | Полный синтезированный ответ исследования |
| `outline` | object \| null | Структура разделов отчёта. `null`, если провайдер не возвращает разбивку |
| `outline.sections` | array | Массив разделов |
| `outline.sections[].title` | string | Заголовок раздела |
| `outline.sections[].content` | string | Текст раздела |
| `outline.sections[].sourceIds` | number[] | Идентификаторы источников из `sources[]`, на которые опирается раздел |
| `sources` | array | Массив источников исследования |
| `sources[].id` | number | Порядковый номер источника |
| `sources[].url` | string | Адрес страницы |
| `sources[].title` | string | Заголовок страницы |
| `sources[].fullContent` | string | Полный текст страницы. Длина ограничена провайдером |
| `sources[].publishedDate` | string \| null | Дата публикации в формате ISO 8601 |
| `follow_up_questions` | string[] \| null | Уточняющие вопросы для углубления исследования. `null`, если провайдер не предоставляет |
| `reasoning` | array \| null | Шаги рассуждения агента. `null`, если провайдер не предоставляет |
| `reasoning[].step` | number | Порядковый номер шага |
| `reasoning[].description` | string | Краткое описание шага |
| `research_id` | string | Идентификатор запроса в формате `wr_<14 цифр>_<8 hex>` |
| `upstream_search_id` | string \| null | Идентификатор у внешнего провайдера. Полезен для разбора инцидентов |
| `cost_vibes` | number | Сколько Ꝟ списано фактически |
| `duration_ms` | number | Длительность обработки запроса в миллисекундах |
| `partial_charge` | boolean | Присутствует, когда параллельные запросы исчерпали остаток баланса до конца текущего списания. Запрос завершился успешно, фактически списано меньше номинальной стоимости — итог в `cost_vibes` |
| `charge_log_failed` | boolean | Присутствует, когда не удалось записать журнал использования. Результат отдан, средства не списаны (`cost_vibes: 0`) |
| `ignored_filters` | string[] | Необязательное. Переданные фильтры, которые движок не смог применить. Например, `includeDomains_truncated`, когда передано больше 20 доменов и список обрезан до 20 |

## Пример потока событий

```
event: start
data: {"research_id":"wr_20260504112304_a1b2c3d4","provider":"tavily"}

event: tool_call
data: {"tool":"web_search","description":"Web research started"}

event: tool_result
data: {"description":"got 8 sources","items_count":8}

event: tool_call
data: {"tool":"content_extraction","description":"reading top sources"}

event: answer_delta
data: {"content":"Cursor 3 — это переработанная среда "}

event: answer_delta
data: {"content":"для AI-разработки [1][2]. Windsurf, в свою очередь, "}

event: done
data: {"query":"Сравни возможности Cursor 3 и Windsurf для AI-кодинга","provider":"tavily","synthesized_answer":"Cursor 3 — это переработанная среда для AI-разработки [1][2]. Windsurf, в свою очередь, делает упор на agentic-flow [3]…","outline":null,"sources":[{"id":1,"url":"https://cursor.com/blog/cursor-3","title":"Meet the new Cursor","fullContent":"…","publishedDate":"2026-04-15T00:00:00Z"}],"follow_up_questions":null,"reasoning":null,"research_id":"wr_20260504112304_a1b2c3d4","upstream_search_id":"tvly-research-abc","cost_vibes":0,"duration_ms":18230}
```

## Пример ответа при ошибке

402 — на балансе не хватает Ꝟ:

```json
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Insufficient vibes for this research request.",
    "userMessage": "Недостаточно vibes — пополните баланс или добавьте свой BYOK-ключ (см. GET /v1/search/providers).",
    "hint": "Add a BYOK key to search for free — see GET /v1/search/providers",
    "required": 25
  }
}
```

Три поля приходят **внутри объекта `error`**, а не на верхнем уровне ответа: `userMessage` — текст на языке пользователя, `hint` — подсказка по дальнейшим действиям, `required` — сколько Ꝟ нужно на этот запрос. Набор общий для `INSUFFICIENT_BALANCE` и `BILLING_FROZEN`. Остальные ситуации — в таблице ошибок ниже.

## Ошибки

Ошибки до открытия SSE-потока возвращаются обычным JSON-ответом. Ошибки, возникшие после установки соединения, приходят как событие `error` в потоке.

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_REQUEST` | Пустой `query`, `query` длиннее 2000 символов, `maxSteps` вне диапазона 1..10, `provider` не из списка поддерживаемых провайдеров, `includeDomains` или `excludeDomains` длиннее 50 элементов |
| 402 | `INSUFFICIENT_BALANCE` | На балансе портала недостаточно Ꝟ для платного провайдера. Перейдите на BYOK или пополните баланс |
| 402 | `BILLING_FROZEN` | Биллинг-аккаунт заморожен |
| 403 | `SCOPE_DENIED` | Ключу не хватает скоупа `vibe:search` |
| 404 | `PROVIDER_NOT_FOUND` | Провайдер с таким идентификатором отсутствует или недоступен |
| 404 | `CREDENTIAL_NOT_FOUND` | Для провайдера нет ни USER, ни PORTAL, ни PLATFORM-ключа |
| 404 | `PROVIDER_DOES_NOT_SUPPORT_RESEARCH` | Запрошен провайдер без поддержки research: `brave`, `z-ai` или `bitrix-search`, когда у движка инстанса нет режима research. Такие провайдеры доступны только в [`POST /v1/search`](/docs/search/run) |
| 429 | `RATE_LIMITED` | Превышен лимит 20 запросов в минуту на портал — общий для всех его API-ключей |
| 503 | `FEATURE_NOT_ENABLED` | Глубокое исследование недоступно на этой платформе |

Ошибки внутри SSE-потока:

| Код | Описание |
|-----|---------|
| `UPSTREAM_ERROR` | Провайдер вернул ошибку во время обработки. Списания нет |
| `PROVIDER_DOES_NOT_SUPPORT_RESEARCH` | Защитная проверка адаптера на стороне сервера — фактически дубль HTTP-кода 404, всплывает при рассинхронизации каталога |
| `INTERNAL_ERROR` | Внутренняя ошибка сервера во время обработки |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Только потоковый режим.** Синхронного варианта `/v1/research` без SSE не существует — глубокое исследование длится 10–60 секунд и не вписывается в обычные HTTP-таймауты прокси. Если клиент не может работать с потоком, накапливайте события на сервере-посреднике и отдавайте их пакетом конечному приложению.

**Лимит 20 запросов в минуту на портал.** Ниже, чем у [`POST /v1/search`](/docs/search/run), потому что один research-запрос потребляет ресурсы провайдера в десятки раз дольше обычного поиска. Лимит общий для всех API-ключей портала — они делят его между собой, поэтому распределение нагрузки по нескольким ключам предел не поднимает. При превышении возвращается `429 RATE_LIMITED`.

**Тариф платформенного research-движка зависит от инстанса.** Сколько Ꝟ стоит режим `research` у платформенного движка, возвращает [`GET /v1/search/providers`](/docs/search/providers) в поле `pricing.research`. Для всех BYOK-провайдеров стоимость на стороне платформы — 0 Ꝟ, оплата идёт напрямую с вашего аккаунта у поставщика.

**Каскад выбора ключа отличается от `/v1/search`.** В [`POST /v1/search`](/docs/search/run) при опущенном `provider` платформа сначала проверяет USER/PORTAL-дефолт по любому провайдеру и только потом уходит в движок по умолчанию инстанса. В research всё иначе: если поле `provider` опущено, сервер **сразу подставляет research-движок по умолчанию, настроенный на инстансе**, не проверяя USER/PORTAL-дефолты по другим провайдерам. Если для этого движка нет ни одного ключа, возвращается `404 CREDENTIAL_NOT_FOUND` — даже если у пользователя есть дефолтный ключ для `exa` или другого research-провайдера. Каскад USER → PORTAL → PLATFORM применяется уже **внутри** выбранного провайдера. Чтобы пойти через другой research-провайдер, передавайте `provider` явно.

**Списание происходит только после успешного завершения потока.** Перед запуском проверяется баланс: при нехватке возвращается `402 INSUFFICIENT_BALANCE` без обращения к провайдеру. При обрыве потока с ошибкой провайдера баланс не меняется — оплачивать нечего.

## Смотрите также

- [Web Search для AI](/docs/search)
- [Поиск (POST /v1/search)](/docs/search/run)
- [Провайдеры](/docs/search/providers)
- [Свои ключи (BYOK)](/docs/search/credentials)
- [Ключи и авторизация](/docs/keys-auth)
- [Ошибки](/docs/errors)
