
## 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`. `advanced` costs more — switch to it only when the `basic` response lacks the data you need, see [How to choose the depth](#how-to-choose-the-depth). 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.

## How to choose the depth

A request without `search_depth` runs in `basic` mode. `advanced` costs more. The prices of both modes on your instance are listed in [`GET /v1/search/providers`](/docs/search/providers).

| Task | Depth |
|------|-------|
| Find a fact, price, date, link or recent news item | `basic` |
| Give a language model context for answering the user | `basic` |
| Requests in a loop, on a schedule, or on every dashboard or widget refresh | `basic` |
| A one-off request when the `basic` response lacks the data you need | `advanced` |
| A summary of one topic across many sources | `advanced` |
| Multi-step research with a report | [`POST /v1/research`](/docs/search/research), not `advanced` in a loop |

**Rule for agents and apps:** send the request with `basic` first. Switch to `advanced` only if the `basic` response lacks the data you need. Do not make `advanced` your default.

A request with `advanced` differs from the examples below only in the `search_depth` value:

```json
{
  "query": "what is new in Cursor IDE",
  "search_depth": "advanced",
  "max_results": 5,
  "lang": "auto"
}
```

**Apps that refresh on their own** — dashboards, widgets, scheduled runs — pay for every request. Spend per period is refresh rate × number of widgets × mode price. Cache the result on your side and do not repeat an identical request more often than the data changes.

## 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": "basic",
    "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": "basic",
    "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: 'basic',
    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: 'basic',
    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]`. Comes back `null` when `include_answer: false`, and for engines that do not support synthesis: the `capabilities.output.synthesizedAnswer` flag in [`GET /v1/search/providers`](/docs/search/providers) shows which engines do |
| `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. For `tavily`, a number from 0 to 1: the higher the value, the more relevant the result. Comes back `null` when the engine does not return a score: the `capabilities.output.relevanceScore` flag in [`GET /v1/search/providers`](/docs/search/providers) shows which engines do |
| `results[].publishedDate` | string \| null | Publication date in ISO 8601 format. Comes back `null` when the engine does not return a date: the `capabilities.output.publishedDate` flag in [`GET /v1/search/providers`](/docs/search/providers) shows which engines do. Engines that support `topic: "news"` populate it for news results |
| `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 |
| `billing_source` | string | Optional. Payment source: `wallet` — wallet, including usage beyond the AI quota, `quota` — Bitrix24 account AI quota, `cowork` — Cowork seat quota, `byok` — your own provider key. Omitted for a free request using a platform key |
| `cost_vibes` | number | How many Ꝟ were actually charged to the wallet. When `billing_source` is `quota`, `cowork` or `byok`, the value is `0` |
| `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 | Optional. `true` if charging or writing the usage log failed. The successful search result is returned to the client. If the charge completed before the log failed, `cost_vibes` retains the amount actually charged to the wallet |

## Response example

```json
{
  "query": "what is new in Cursor IDE",
  "provider": "bitrix-search",
  "search_depth": "basic",
  "answer": "Cursor 3 — a redesign of the IDE interface. An agents window was added.",
  "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",
  "billing_source": "wallet",
  "cost_vibes": 5,
  "duration_ms": 8523
}
```

The `provider` value in the example (`bitrix-search`) depends on the instance: without an explicit `provider`, the request is handled by the default engine configured on the instance. The `defaultProvider` field in [`GET /v1/me`](/docs/keys-auth) shows the engine, and [`GET /v1/search/providers`](/docs/search/providers) shows its pricing. The example shows a wallet charge. When using AI quota or Cowork seat quota, `cost_vibes` is `0`. The payment source selection order is described under [Pricing](/docs/search#pricing).

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, including `billing_source` and `cost_vibes` |
| `error` | A failure during processing | `error.code`, `error.message` |

Example stream:

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

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

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

Quota refusals arrive as regular HTTP responses with an `error` object before the SSE stream opens. Check the HTTP status and `error.code` before reading stream events.

| 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` outside the list of supported providers, an 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 |
| 402 | `AI_QUOTA_EXHAUSTED` | AI quota is exhausted and wallet overage is unavailable. The reason is in `error.reason`, and the reset time is in `error.resetAt`. Wait for the reset, enable overage in your Vibecode account and top up the wallet, or use your own provider key. The optional `error.learnMore` field contains additional information |
| 429 | `AI_PACING_LIMITED` | The AI quota pacing limit is reached. The response includes `error.reason`, `error.overageDenied`, `error.resetAt` and the `Retry-After` header in seconds. Wait for the specified time or use your own provider key |
| 402 | `COWORK_QUOTA_EXHAUSTED` | A Cowork seat quota window is exhausted. The `error.window` field is `fiveHour`, `week` or `month`. Wait until `error.resetAt` or for the delay in the `Retry-After` header in seconds, or use your own provider key |
| 402 | `COWORK_SUBSCRIPTION_INACTIVE` | There is no active Cowork seat. Activate or resume the seat, or use your own provider key |
| 402 | `company_budget_exhausted` | The monthly company spend budget set by the portal administrator is exhausted. The `scope` field names the budget that was hit: `USER` — the caller's own budget, `PORTAL` — the budget of the whole portal. The `canRequest` field tells whether an increase can be requested: `true` for a personal budget, `false` for the portal one, which only an administrator raises. The rejection arrives when paying from the wallet, including usage beyond the AI quota. A search made with your own BYOK key is free and keeps working |
| 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 no USER, PORTAL, or 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. The current limit is returned in the `x-ratelimit-limit` header |
| 429 | `SEARCH_DAILY_CAP_REACHED` | With this request, total search usage from the wallet, AI quota and Cowork seat quota for the current day in UTC+3 would exceed the cap set by the Bitrix24 account admin. The provider is not called and nothing is charged. The cap is shared by `/v1/search` and [`POST /v1/research`](/docs/search/research). The response carries a `Retry-After` header, and the error body carries `resetAt` — the start of the next day in UTC+3 — and `hint`. Searches on your own BYOK key are not limited by the cap
| 500 | `INTERNAL_ERROR` | Internal server error. The response contains `search_id` |
| 401/403/429/502 | `UPSTREAM_ERROR` | The provider returned an error; no charge. The `upstream_status` field in the response carries the provider's original status. For a BYOK key, provider `401` and `403` responses keep their status — the provider rejected your key, so retrying without replacing it will not help. A provider `429` keeps its status for any key and carries the `Retry-After` header — retry later. Every other provider error, including a rejected platform-engine key, arrives as `502` — retry the request |
| 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 |

For the full list of common API errors, see [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 calling the provider, the platform checks whether payment can proceed from the selected source. Wallet payments, including AI quota overage, require balance and company budget checks. Provider errors consume neither quota nor wallet funds.

**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.** When the provider found no sources and returned `results: []`, the successful request consumes quota or wallet funds according to the selected payment source. The charge covers the provider's processing of the request, not the number of results.

**An accounting failure does not invalidate the search result.** When `charge_log_failed: true`, do not resend the request just to write the usage log: another search is a separate request with a separate charge.

**`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).

**A Bitrix24 account admin can turn off the `advanced` mode.** Paid search through the platform engine with `search_depth: "advanced"` then runs as `basic`, and the request is not rejected. The response and the SSE `start` and `done` events carry `search_depth: "basic"`, and the response headers, synchronous and streaming alike, include `X-Search-Depth-Downgraded: advanced`. The charge follows the `basic` price, and the request counts toward the daily spend cap at that price. Search on your own BYOK key is not affected.

## See also

- [Web Search for AI](/docs/search)
- [Deep Research (POST /v1/research)](/docs/search/research)
- [Providers](/docs/search/providers)
- [Your own keys (BYOK)](/docs/search/credentials)
- [Web search + LLM (RAG)](/docs/recipes/web-search-with-llm)
- [Keys and authorization](/docs/keys-auth)
- [Errors](/docs/errors)
- [Limits and optimization](/docs/optimization)
