
## Search

`POST /v1/search`

Performs a web search and returns a synthesized answer with links to sources. Supports synchronous mode and streaming via Server-Sent Events (`stream: true`). Deep research with a multi-step agentic loop is a separate endpoint [`POST /v1/research`](/docs/search/research).

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|---------|
| `query` | string | yes | — | Query text. From 1 to 400 characters |
| `search_depth` | string | no | `basic` | Search depth: `basic` or `advanced`. The price of each mode is listed in [`GET /v1/search/providers`](/docs/search/providers) |
| `provider` | string | no | cascade USER → PORTAL → instance default engine | Forced engine choice: one of `bitrix-search`, `tavily`, `brave`, `exa`, `you-com`, `linkup`, `perplexity`, `z-ai`. `jina` is available only in [`POST /v1/research`](/docs/search/research) |
| `topic` | string | no | `general` | Search topic: `general` or `news`. Not all engines support the `news` mode — those that do populate `publishedDate` for news results, while the rest ignore it |
| `max_results` | number | no | 5 | Number of results in the response. From 1 to 20 |
| `max_steps` | number | no | 3 | Maximum number of steps in agentic mode. From 1 to 5. Honored only by engines that support it — for `bitrix-search` this depends on the instance engine |
| `lang` | string | no | `ru` | Search language: `ru`, `en` or `auto`. The default engine ignores this field — see "Known specifics" |
| `include_answer` | boolean | no | `true` | Include the synthesized answer in the `answer` field |
| `include_raw_content` | boolean | no | `false` | Request the full text of the pages found. For engines that support it, the text arrives in `results[].rawContent` |
| `include_images` | boolean | no | `false` | Request images for the query. For engines that support it, the links arrive in the top-level `images` array |
| `include_domains` | string[] | no | `[]` | Restrict the search to the domains in the list. Up to 10 hostnames |
| `exclude_domains` | string[] | no | `[]` | Exclude domains. Up to 10 hostnames |
| `time_range` | string \| null | no | `null` | Publication time window: `day`, `week`, `month`, `year` |
| `stream` | boolean | no | `false` | `true` — streaming via SSE instead of a JSON response |

Not all providers support every filter — see the table in [Web Search for AI](/docs/search#supported-provider-capabilities). An unsupported filter is ignored, and the response carries an `X-Search-Filters-Ignored` header with the list of skipped fields.

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/search \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "what is new in Cursor IDE",
    "search_depth": "advanced",
    "max_results": 5,
    "lang": "auto"
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/search \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "what is new in Cursor IDE",
    "search_depth": "advanced",
    "max_results": 5,
    "lang": "auto"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/search', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'what is new in Cursor IDE',
    search_depth: 'advanced',
    max_results: 5,
    lang: 'auto',
  }),
})

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

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/search', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'what is new in Cursor IDE',
    search_depth: 'advanced',
    max_results: 5,
    lang: 'auto',
  }),
})

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

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `query` | string | The original query text |
| `provider` | string | Identifier of the provider that processed the request (e.g. `bitrix-search`, `tavily`) |
| `search_depth` | string | The applied search depth: `basic` or `advanced` |
| `answer` | string \| null | Synthesized answer. Engines with citation support mark the sources with `[N]` — see the capability table in the section index. `null` for `brave` or when `include_answer: false` |
| `results` | array | Array of found sources |
| `results[].id` | number | Sequential number. Matches the `[N]` citation marker in `answer` for engines that emit citations |
| `results[].url` | string | Page address |
| `results[].title` | string | Page title |
| `results[].content` | string | Always a short description or snippet of the page. The full text arrives in a separate `rawContent` field, not here |
| `results[].rawContent` | string \| null | The full page text when `include_raw_content: true` for engines that support it. Size capped at roughly 40 KB per result. `null` when the full text was not requested or the engine does not return it |
| `results[].score` | number \| null | Relevance score from the provider — a number from 0 to 1: the higher the value, the more relevant the result. `null` for `brave`. For `bitrix-search` it depends on the instance engine — see the capability table in the section index |
| `results[].publishedDate` | string \| null | Publication date in ISO 8601 format. Populated mainly when `topic: "news"`. For `bitrix-search` it depends on the instance engine — see the capability table in the section index |
| `images` | array | Top-level array of images when `include_images: true` for engines that support it. Each element is an object with a `url` field. Empty array when images were not requested or the engine does not return them |
| `ignored_filters` | string[] | The passed filters and parameters the engine could not apply. The same list as the `X-Search-Filters-Ignored` header, delivered in the response body. May contain `topic`, `include_images`, `include_domains`, `time_range`, and others — the set depends on the engine |
| `search_id` | string | Request identifier in the format `ws_<14 digits>_<8 hex>` |
| `upstream_search_id` | string \| null | Identifier at the external provider. Useful for incident investigation |
| `cost_vibes` | number | How many Ꝟ were actually charged |
| `duration_ms` | number | Request processing duration in milliseconds |
| `partial_charge` | boolean | Present when parallel requests drained the remaining balance before the current charge completed. The request still succeeded, and the amount actually charged is less than the nominal cost — the total is in `cost_vibes` |
| `charge_log_failed` | boolean | Present when the usage log could not be written. The result was delivered, no funds were charged (`cost_vibes: 0`) |

## Response example

```json
{
  "query": "what is new in Cursor IDE",
  "provider": "bitrix-search",
  "search_depth": "advanced",
  "answer": "Cursor 3 — a redesign of the IDE interface [1]. An agents window was added [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
}
```

The `provider` value in the example (`bitrix-search`) and `cost_vibes` depend on the instance: without an explicit `provider`, the request is handled by the default engine configured on the instance (shown by the `defaultProvider` field in [`GET /v1/me`](/docs/keys-auth)), and the charged amount is its price from [`GET /v1/search/providers`](/docs/search/providers).

With `include_raw_content: true` on a supporting engine the `rawContent` field is populated with the full page text, and with `include_images: true` the top-level `images` array is populated with image links. If the engine does not support `topic` or `include_images`, those parameters land in `ignored_filters`.

**Output safety.** `rawContent` is unsanitized text from the open web, and `images[]` are third-party URLs. Before rendering in a browser, sanitize the text and proxy or validate the image links — do not insert them into the DOM as-is.

## Streaming (SSE)

With `stream: true` the response arrives as an event stream with the `text/event-stream` content type. Each event is an `event: <type>` + `data: <JSON>` pair, separated by a blank line.

Possible events:

| Event | When it occurs | `data` fields |
|---------|-----------------|-------------|
| `start` | At the start of the request | `search_id`, `provider`, `search_depth` |
| `thinking` | Intermediate agent reasoning | `content` — a text fragment |
| `tool_call` | The agent called an internal tool | `tool` (`web_search` / `content_extraction` / `external_tool`), `description` |
| `tool_result` | The tool returned a result | `description`, `items_count` |
| `answer_delta` | The next fragment of the final answer | `content` — a chunk of text |
| `done` | The final block | the same fields as in the synchronous response |
| `error` | A failure during processing | `error.code`, `error.message` |

Example stream:

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

event: thinking
data: {"content":"Let's look into the sources now"}

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":"a redesign of the IDE interface [1]."}

event: done
data: {"query":"what is new in Cursor IDE","provider":"bitrix-search","search_depth":"advanced","answer":"Cursor 3 — a redesign of the IDE interface [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}
```

Whether a real event stream with intermediate `thinking` / `tool_call` / `answer_delta` is emitted depends on the engine: for `bitrix-search` this varies by instance, while the other providers stream in buffered mode — only `start` and `done` arrive. A progressive stream is indicated by the `capabilities.modes.streaming` field in [`GET /v1/search/providers`](/docs/search/providers).

## Error response example

402 — insufficient balance:

```json
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Insufficient vibes for this search.",
    "userMessage": "Insufficient vibes — top up your balance or add your own BYOK key (see GET /v1/search/providers).",
    "hint": "Add a BYOK key to search for free — see GET /v1/search/providers",
    "required": 5
  }
}
```

The `required` field is the amount in Ꝟ needed for the request. The other situations are listed in the error table below.

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_REQUEST` | Empty `query`, `query` longer than 400 characters, `max_results` outside the 1..20 range, `max_steps` outside the 1..5 range, `provider` not from the list of supported providers, unknown value for `search_depth`, `topic`, `lang` or `time_range`, `include_domains` / `exclude_domains` longer than 10 elements |
| 402 | `INSUFFICIENT_BALANCE` | The portal balance does not have enough Ꝟ for the selected mode |
| 402 | `BILLING_FROZEN` | The billing account is frozen |
| 403 | `SCOPE_DENIED` | The key lacks the `vibe:search` scope |
| 404 | `PROVIDER_NOT_FOUND` | The `provider` passed does not exist in the system or is disabled |
| 404 | `CREDENTIAL_NOT_FOUND` | The provider has neither a USER, PORTAL, nor PLATFORM key |
| 404 | `PROVIDER_DOES_NOT_SUPPORT_SEARCH` | `provider: "jina"` was requested — Jina works only in [`POST /v1/research`](/docs/search/research). The `search_id` field is present in the response |
| 429 | `RATE_LIMITED` | Exceeded the limit of 60 requests per minute per portal, shared across all of the portal's API keys |
| 500 | `INTERNAL_ERROR` | Internal server error. The response contains `search_id` |
| 502 | `UPSTREAM_ERROR` | The provider returned an error. No charge |
| 503 | `FEATURE_NOT_ENABLED` | Web Search is unavailable on this platform |
| 503 | `UPSTREAM_TIMEOUT` | The provider exceeded the request timeout. No charge. The response carries the `Retry-After: 30` header — retry the request after the time it specifies |

The full list of common API errors — [Errors](/docs/errors).

## Known specifics

**Key selection cascade.** When the `provider` field is omitted, the platform picks the source in the order: user key (USER BYOK) → Bitrix24 account key (PORTAL BYOK) → the default engine configured on the instance (shown by the `defaultProvider` field in [`GET /v1/me`](/docs/keys-auth)). Your own BYOK key with `isDefault: true` is used automatically — you do not need to pass `provider`.

**The charge happens only after a successful response.** Before the request the balance is checked: if insufficient, `402 INSUFFICIENT_BALANCE` is returned without calling the provider. On `UPSTREAM_ERROR` or `UPSTREAM_TIMEOUT` the balance does not change — the provider returned no result, so there is nothing to charge for.

**The default engine ignores `lang` in `/v1/search`.** It does not forward `lang` to the upstream provider — the field always appears in `X-Search-Filters-Ignored`.

**An empty result set is charged in full.** When the provider found no sources and returned `results: []`, `cost_vibes` is still charged in full. This is by design: the charge covers the provider's processing of the request, not the number of results.

**The `charge_log_failed` field signals a lost audit record.** If the provider responds successfully but the usage log cannot be written, the response is still delivered to the client with `cost_vibes: 0` and the `charge_log_failed: true` flag. No Vibe credits were charged — the audit log for this request is also absent. The scenario is rare and is visible in the AI Search section (`/search`).

**`provider: "jina"` is available only in `/v1/research`.** A request with `provider: "jina"` on the current endpoint returns `404 PROVIDER_DOES_NOT_SUPPORT_SEARCH`. The list of providers supporting `/v1/search` is checked via the `capabilities.modes.search.basic` or `capabilities.modes.search.advanced` field in [`GET /v1/search/providers`](/docs/search/providers).

## See also

- [Web Search for AI](/docs/search) — section overview, pricing, migration from Tavily
- [Deep Research (POST /v1/research)](/docs/search/research) — multi-step agentic research
- [Providers](/docs/search/providers) — list of engines with prices and capabilities
- [Your own keys (BYOK)](/docs/search/credentials) — adding BYOK keys for eight providers
- [Web search + LLM (RAG)](/docs/recipes/web-search-with-llm) — recipe pairing with `/v1/ai/chat/completions` for answers with citations
- [Keys and authorization](/docs/keys-auth) — personal key and authorization key
- [Errors](/docs/errors) — API error code reference
- [Limits and optimization](/docs/optimization) — common request rate rules
