
## Поиск

`POST /v1/search`

Выполняет поиск в интернете и возвращает синтезированный ответ со ссылками на источники. Поддерживает синхронный режим и потоковую передачу через Server-Sent Events (`stream: true`). Глубокое исследование с многошаговым агентным циклом — отдельный эндпоинт [`POST /v1/research`](/docs/search/research).

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

| Поле | Тип | Обяз. | По умолч. | Описание |
|------|-----|:-----:|-----------|---------|
| `query` | string | да | — | Текст запроса. От 1 до 400 символов |
| `search_depth` | string | нет | `basic` | Глубина поиска: `basic` или `advanced`. Цена режима — в [`GET /v1/search/providers`](/docs/search/providers) |
| `provider` | string | нет | каскад USER → PORTAL → движок по умолчанию инстанса | Принудительный выбор движка: один из `bitrix-search`, `tavily`, `brave`, `exa`, `you-com`, `linkup`, `perplexity`, `z-ai`. `jina` доступен только в [`POST /v1/research`](/docs/search/research) |
| `topic` | string | нет | `general` | Тематика поиска: `general` или `news`. Режим `news` поддерживают не все движки — у поддерживающих он наполняет `publishedDate` для новостных результатов, у остальных игнорируется |
| `max_results` | number | нет | 5 | Количество результатов в ответе. От 1 до 20 |
| `max_steps` | number | нет | 3 | Максимум шагов агентного режима. От 1 до 5. Учитывает только `bitrix-search` |
| `lang` | string | нет | `ru` | Язык поиска: `ru`, `en` или `auto` |
| `include_answer` | boolean | нет | `true` | Включить синтезированный ответ в поле `answer` |
| `include_raw_content` | boolean | нет | `false` | Запросить полный текст найденных страниц. У поддерживающих движков текст приходит в `results[].rawContent` |
| `include_images` | boolean | нет | `false` | Запросить изображения по запросу. У поддерживающих движков ссылки приходят в верхнеуровневом массиве `images` |
| `include_domains` | string[] | нет | `[]` | Поиск только по доменам из списка. До 10 имён хоста |
| `exclude_domains` | string[] | нет | `[]` | Исключить домены. До 10 имён хоста |
| `time_range` | string \| null | нет | `null` | Окно времени публикации: `day`, `week`, `month`, `year` |
| `stream` | boolean | нет | `false` | `true` — потоковая передача через SSE вместо JSON-ответа |

Часть фильтров поддерживается не всеми провайдерами — см. таблицу в [Web Search для AI](/docs/search#поддерживаемые-возможности-провайдеров). Неподдерживаемый фильтр игнорируется, в ответе появляется заголовок `X-Search-Filters-Ignored` со списком пропущенных полей.

## Примеры

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

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/search \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "что нового в Cursor IDE",
    "search_depth": "advanced",
    "max_results": 5,
    "lang": "ru"
  }'
```

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

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/search \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "что нового в Cursor IDE",
    "search_depth": "advanced",
    "max_results": 5,
    "lang": "ru"
  }'
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/search', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'что нового в Cursor IDE',
    search_depth: 'advanced',
    max_results: 5,
    lang: 'ru',
  }),
})

const data = await res.json()
console.log(data.answer)
data.results.forEach((r) => console.log(`[${r.id}] ${r.title} — ${r.url}`))
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/search', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'что нового в Cursor IDE',
    search_depth: 'advanced',
    max_results: 5,
    lang: 'ru',
  }),
})

const data = await res.json()
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|---------|
| `query` | string | Текст исходного запроса |
| `provider` | string | Идентификатор провайдера, который обработал запрос (например, `bitrix-search`, `tavily`) |
| `search_depth` | string | Применённая глубина поиска: `basic` или `advanced` |
| `answer` | string \| null | Синтезированный ответ с маркерами `[N]`. `null` для `brave` или при `include_answer: false` |
| `results` | array | Массив найденных источников |
| `results[].id` | number | Порядковый номер. Совпадает с маркером `[N]` в `answer` |
| `results[].url` | string | Адрес страницы |
| `results[].title` | string | Заголовок страницы |
| `results[].content` | string | Всегда краткое описание или сниппет страницы. Полный текст приходит отдельным полем `rawContent`, а не здесь |
| `results[].rawContent` | string \| null | Полный текст страницы при `include_raw_content: true` у поддерживающих движков. Размер ограничен примерно 40 КБ на результат. `null`, если полный текст не запрашивался или движок его не отдаёт |
| `results[].score` | number \| null | Оценка релевантности от провайдера. Для `tavily` — число от 0 до 1, чем выше — тем релевантнее. `null` для `bitrix-search` и `brave` |
| `results[].publishedDate` | string \| null | Дата публикации в формате ISO 8601. `null` для `bitrix-search`. У `tavily` заполняется в основном при `topic: "news"` |
| `images` | array | Верхнеуровневый массив изображений при `include_images: true` у поддерживающих движков. Каждый элемент — объект с полем `url`. Пустой массив, если изображения не запрашивались или движок их не отдаёт |
| `ignored_filters` | string[] | Переданные фильтры и параметры, которые движок не смог применить. Дублирует заголовок `X-Search-Filters-Ignored` в теле ответа. Может содержать `topic`, `include_images`, `include_domains`, `time_range` и другие — состав зависит от движка |
| `search_id` | string | Идентификатор запроса в формате `ws_<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`) |

## Пример ответа

```json
{
  "query": "что нового в Cursor IDE",
  "provider": "bitrix-search",
  "search_depth": "advanced",
  "answer": "Cursor 3 — переработка интерфейса IDE [1]. Появилось окно агентов [2].",
  "results": [
    {
      "id": 1,
      "url": "https://cursor.com/blog/cursor-3",
      "title": "Meet the new Cursor",
      "content": "Cursor 3 brings a new agent-first interface...",
      "rawContent": null,
      "score": null,
      "publishedDate": null
    },
    {
      "id": 2,
      "url": "https://cursor.com/changelog",
      "title": "Changelog",
      "content": "Latest features in Cursor IDE...",
      "rawContent": null,
      "score": null,
      "publishedDate": null
    }
  ],
  "images": [],
  "ignored_filters": [],
  "search_id": "ws_20260430113025_a1b2c3d4",
  "upstream_search_id": "AG_xyz",
  "cost_vibes": 5,
  "duration_ms": 8523
}
```

Значение `provider` в примере (`bitrix-search`) и `cost_vibes` зависят от инстанса: без явного `provider` запрос обрабатывает движок по умолчанию, настроенный на инстансе (его показывает поле `defaultProvider` в [`GET /v1/me`](/docs/keys-auth)), а сумма списания — его тариф из [`GET /v1/search/providers`](/docs/search/providers).

При `include_raw_content: true` у поддерживающего движка поле `rawContent` заполняется полным текстом страницы, при `include_images: true` верхнеуровневый массив `images` — ссылками на изображения. Если движок не поддерживает `topic` или `include_images`, эти параметры попадают в `ignored_filters`.

**Безопасность вывода.** `rawContent` — это неочищенный текст из открытого интернета, а `images[]` — сторонние URL. Перед показом в браузере очищайте текст и проксируйте или проверяйте ссылки изображений — не вставляйте их в DOM как есть.

## Потоковая передача (SSE)

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

Возможные события:

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

Пример потока:

```
event: start
data: {"search_id":"ws_20260430113025_a1b2c3d4","provider":"bitrix-search","search_depth":"advanced"}

event: thinking
data: {"content":"Сейчас посмотрим в источниках"}

event: tool_call
data: {"tool":"web_search","description":"web search query"}

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

event: answer_delta
data: {"content":"Cursor 3 — "}

event: answer_delta
data: {"content":"переработка интерфейса IDE [1]."}

event: done
data: {"query":"что нового в Cursor IDE","provider":"bitrix-search","search_depth":"advanced","answer":"Cursor 3 — переработка интерфейса IDE [1].","results":[{"id":1,"url":"https://cursor.com/blog/cursor-3","title":"Meet the new Cursor","content":"...","rawContent":null,"score":null,"publishedDate":null}],"images":[],"ignored_filters":[],"search_id":"ws_20260430113025_a1b2c3d4","upstream_search_id":"AG_xyz","cost_vibes":5,"duration_ms":8523}
```

Реальный поток событий с промежуточными `thinking` / `tool_call` / `answer_delta` отправляет только `bitrix-search`. Для остальных провайдеров поток буферизованный — приходит только `start` и `done`. Признак прогрессивного потока — поле `capabilities.modes.streaming` в [`GET /v1/search/providers`](/docs/search/providers).

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

402 — недостаточно средств:

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

Поле `required` — сумма в Ꝟ, необходимая для запроса. Остальные ситуации перечислены в таблице ошибок ниже.

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_REQUEST` | Пустой `query`, `query` длиннее 400 символов, `max_results` вне диапазона 1..20, `max_steps` вне диапазона 1..5, `provider` не из списка поддерживаемых провайдеров, неизвестное значение `search_depth`, `topic`, `lang` или `time_range`, `include_domains` / `exclude_domains` длиннее 10 элементов |
| 402 | `INSUFFICIENT_BALANCE` | На балансе портала недостаточно Ꝟ для выбранного режима |
| 402 | `BILLING_FROZEN` | Биллинг-аккаунт заморожен |
| 403 | `SCOPE_DENIED` | Ключу не хватает скоупа `vibe:search` |
| 404 | `PROVIDER_NOT_FOUND` | Передан `provider`, которого нет в системе или он отключён |
| 404 | `CREDENTIAL_NOT_FOUND` | Для провайдера нет ни USER, ни PORTAL, ни PLATFORM-ключа |
| 404 | `PROVIDER_DOES_NOT_SUPPORT_SEARCH` | Запрошен `provider: "jina"` — Jina работает только в [`POST /v1/research`](/docs/search/research). Поле `search_id` присутствует в ответе |
| 429 | `RATE_LIMITED` | Превышен лимит 60 запросов в минуту на портал — общий для всех его API-ключей |
| 500 | `INTERNAL_ERROR` | Внутренняя ошибка сервера. В ответе присутствует `search_id` |
| 401/403/429/502 | `UPSTREAM_ERROR` | Провайдер вернул ошибку, списания нет; поле `upstream_status` в ответе несёт исходный статус провайдера. Для BYOK-ключа ответы провайдера `401` и `403` приходят с тем же статусом — провайдер отверг ваш ключ, повтор без его замены не поможет. Ответ провайдера `429` сохраняет статус для любого ключа и несёт заголовок `Retry-After` — повторите позже. Остальные ошибки провайдера, включая отказ ключа платформенного движка, приходят как `502` — повторите запрос |
| 503 | `FEATURE_NOT_ENABLED` | Web Search недоступен на этой платформе |
| 503 | `UPSTREAM_TIMEOUT` | Провайдер превысил время ожидания запроса. Списания нет. Ответ содержит заголовок `Retry-After: 30` — повторите запрос через указанное в нём время |

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

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

**Каскад выбора ключа.** Когда поле `provider` опущено, платформа подбирает источник в порядке: ключ пользователя (USER BYOK) → ключ портала (PORTAL BYOK) → движок по умолчанию, настроенный на инстансе (его показывает поле `defaultProvider` в [`GET /v1/me`](/docs/keys-auth)). Свой BYOK-ключ с `isDefault: true` начинает срабатывать автоматически — `provider` указывать необязательно.

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

**`lang: "auto"` для `bitrix-search` приводится к `ru`.** Значения `ru` и `en` передаются движку как есть — для англоязычной выдачи передайте `lang: "en"`. Поле не отбрасывается и в `X-Search-Filters-Ignored` не попадает.

**Пустая выдача оплачивается полностью.** Когда провайдер не нашёл ни одного источника и вернул `results: []`, `cost_vibes` всё равно списывается в полном размере. Это поведение по дизайну — оплачивается обработка запроса провайдером, а не количество результатов.

**Поле `charge_log_failed` сигнализирует о потерянной аудит-записи.** При успешном ответе провайдера, но сбое записи журнала использования, ответ всё равно отдаётся клиенту с `cost_vibes: 0` и флагом `charge_log_failed: true`. Списание Вайбов не произошло — аудит-журнал по этому запросу также отсутствует. Сценарий редкий, виден в разделе «AI-Поиск» (`/search`).

**`provider: "jina"` доступен только в `/v1/research`.** Запрос с `provider: "jina"` в текущем эндпоинте возвращает `404 PROVIDER_DOES_NOT_SUPPORT_SEARCH`. Список провайдеров, поддерживающих `/v1/search`, проверяется по полю `capabilities.modes.search.basic` или `capabilities.modes.search.advanced` в [`GET /v1/search/providers`](/docs/search/providers).

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

- [Web Search для AI](/docs/search)
- [Глубокий поиск (POST /v1/research)](/docs/search/research)
- [Провайдеры](/docs/search/providers)
- [Свои ключи (BYOK)](/docs/search/credentials)
- [Веб-поиск + LLM (RAG)](/docs/recipes/web-search-with-llm)
- [Ключи и авторизация](/docs/keys-auth)
- [Ошибки](/docs/errors)
- [Лимиты и оптимизация](/docs/optimization)
