
## Deep research

`POST /v1/research`

Launches a deep iterative search driven by a multi-step agentic loop: search → read → analyze. Returns a structured report with sections, sources, and optional follow-up questions. The endpoint always works in streaming mode via Server-Sent Events — a single request lasts 10–60 seconds and does not fit into a typical synchronous HTTP timeout.

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|---------|
| `query` | string | yes | — | Research query text. From 1 to 2000 characters |
| `provider` | string | no | instance default research engine | A provider with research support: `tavily`, `exa`, `you-com`, `linkup`, `perplexity`, or `jina`. The platform-managed `bitrix-search` supports research depending on the instance. For the current list, see [`GET /v1/search/providers`](/docs/search/providers) — the providers with `capabilities.modes.research: true` |
| `maxSteps` | number | no | 5 | Maximum number of agentic-loop steps. From 1 to 10. The adapter caps the value if the external provider does not accept that many steps |
| `lang` | string | no | `auto` | Research language: `ru`, `en`, or `auto` |
| `includeDomains` | string[] | no | `[]` | Search only within the listed domains. Up to 50 host names. Some engines take no more than 20 — on overflow the list is truncated, and the `done` frame carries `ignored_filters` with `includeDomains_truncated` |
| `excludeDomains` | string[] | no | `[]` | Exclude domains. Up to 50 host names. Some engines take no more than 20 — on overflow the list is truncated, and the `done` frame carries `ignored_filters` with `excludeDomains_truncated` |

## Examples

### curl — personal key

```bash
curl -N -X POST https://vibecode.bitrix24.com/v1/research \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Compare the capabilities of Cursor 3 and Windsurf for AI coding",
    "provider": "tavily",
    "maxSteps": 5,
    "lang": "en"
  }'
```

### curl — OAuth application

```bash
curl -N -X POST https://vibecode.bitrix24.com/v1/research \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Compare the capabilities of Cursor 3 and Windsurf for AI coding",
    "provider": "tavily",
    "maxSteps": 5,
    "lang": "en"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/research', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Compare the capabilities of Cursor 3 and Windsurf for AI coding',
    provider: 'tavily',
    maxSteps: 5,
    lang: 'en',
  }),
})

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('Done:', data.synthesized_answer.slice(0, 200))
      console.log('Sources:', data.sources.length)
    }
  }
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/research', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'Compare the capabilities of Cursor 3 and Windsurf for AI coding',
    provider: 'tavily',
    maxSteps: 5,
    lang: 'en',
  }),
})

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('Done:', data.synthesized_answer.slice(0, 200))
      console.log('Sources:', data.sources.length)
    }
  }
}
```

## Response fields

The response arrives as a `text/event-stream`. Each event is a pair `event: <type>` + `data: <JSON>`, separated by a blank line. The stream is guaranteed to end with one of two events: `done` (success) or `error` (failure).

### Stream events

| Event | When it occurs | `data` fields |
|---------|-----------------|-------------|
| `start` | At the start of the request | `research_id`, `provider` |
| `thinking` | Intermediate agent reasoning | `content` — a text fragment |
| `tool_call` | The agent invoked an internal tool | `tool` (`web_search` / `content_extraction` / `external_tool`), `description` |
| `tool_result` | A tool returned a result | `description`, `items_count` |
| `section_complete` | A research section is finished, with its sources attached | `section.title`, `section.content`, `section.sourceIds` |
| `answer_delta` | The next fragment of the final answer | `content` — a chunk of text |
| `done` | Final block | see the "Fields of the `done` event" table below |
| `error` | Failure during processing | `error.code`, `error.message` |

Which intermediate events arrive depends on the provider. None of them is mandatory: receiving `start` and `done` is enough for a normal completion.

### Fields of the `done` event

| Field | Type | Description |
|------|-----|---------|
| `query` | string | The original query text |
| `provider` | string | Identifier of the provider that processed the request (for example, `tavily`, `exa`) |
| `synthesized_answer` | string | The full synthesized research answer |
| `outline` | object \| null | The structure of the report's sections. `null` if the provider does not return a breakdown |
| `outline.sections` | array | Array of sections |
| `outline.sections[].title` | string | Section title |
| `outline.sections[].content` | string | Section text |
| `outline.sections[].sourceIds` | number[] | Identifiers of the sources from `sources[]` that the section relies on |
| `sources` | array | Array of research sources |
| `sources[].id` | number | Sequential source number |
| `sources[].url` | string | Page URL |
| `sources[].title` | string | Page title |
| `sources[].fullContent` | string | Full page text. Length is capped by the provider |
| `sources[].publishedDate` | string \| null | Publication date in ISO 8601 format |
| `follow_up_questions` | string[] \| null | Follow-up questions for deepening the research. `null` if the provider does not return them |
| `reasoning` | array \| null | The agent's reasoning steps. `null` if the provider does not return them |
| `reasoning[].step` | number | Sequential step number |
| `reasoning[].description` | string | Short step description |
| `research_id` | string | Request identifier in the format `wr_<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 exhausted the remaining balance before the end of the current charge. The request completed successfully, but less than the nominal cost was actually charged — the total is in `cost_vibes` |
| `charge_log_failed` | boolean | Optional. `true` if charging or writing the usage log failed. The successful research result is returned to the client. If the charge completed before the log failed, `cost_vibes` retains the amount actually charged to the wallet |
| `ignored_filters` | string[] | Optional. The filters that were passed but that the engine could not apply. For example, `includeDomains_truncated` when more than 20 domains were passed and the list was truncated to 20 |

## Response example

```
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":"Web research completed in 18s","items_count":8}

event: done
data: {"query":"Compare the capabilities of Cursor 3 and Windsurf for AI coding","provider":"tavily","synthesized_answer":"Cursor 3 is a reworked environment for AI development [1][2]. Windsurf, in turn, focuses on 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,"ignored_filters":["lang"],"research_id":"wr_20260504112304_a1b2c3d4","upstream_search_id":"tvly-research-abc","billing_source":"byok","cost_vibes":0,"duration_ms":18230}
```

## Error response example

402 — not enough Ꝟ on the balance:

```json
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Insufficient vibes for this research request. Top up your account to continue.",
    "userMessage": "Insufficient Vibe credits — 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": 25,
    "topupUrl": "https://vibecode.bitrix24.com/r/upgrade?c=ai_quota_copay&sc=api&m=api&t=topup&p=…&e=…&s=…"
  }
}
```

Four fields arrive **inside the `error` object**, not at the top level of the response: `userMessage` — text in the user's language, `hint` — guidance on next steps, `required` — how many Ꝟ this request needs, `topupUrl` — a link to top up the balance. The same set is used for `INSUFFICIENT_BALANCE` and `BILLING_FROZEN`. All other cases are listed in the error table below.

## Errors

Errors that occur before the SSE stream opens are returned as a regular JSON response. Errors that occur after the connection is established arrive as an `error` event in the stream.

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_REQUEST` | Empty `query`, `query` longer than 2000 characters, `maxSteps` outside the 1..10 range, `provider` not in the list of supported providers, `includeDomains` or `excludeDomains` with more than 50 elements |
| 402 | `INSUFFICIENT_BALANCE` | The portal balance has insufficient Ꝟ for a paid provider. Switch to BYOK or top up the balance |
| 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. Research on your own BYOK key is free and keeps working |
| 403 | `SCOPE_DENIED` | The key lacks the `vibe:search` scope |
| 404 | `PROVIDER_NOT_FOUND` | A provider with this identifier does not exist or is unavailable |
| 404 | `CREDENTIAL_NOT_FOUND` | There is no USER, PORTAL, or PLATFORM key for the provider |
| 404 | `PROVIDER_DOES_NOT_SUPPORT_RESEARCH` | A provider without research support was requested: `brave`, `z-ai`, or `bitrix-search` when the instance engine has no research mode. Such providers are available only in [`POST /v1/search`](/docs/search/run) |
| 429 | `RATE_LIMITED` | The limit of 20 requests per minute per portal — shared by all of its API keys — was exceeded. 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 rejection is returned as a regular JSON response before the stream opens. The provider is not called and nothing is charged. The cap is shared by `/v1/research` and [`POST /v1/search`](/docs/search/run). The response carries a `Retry-After` header, and the error body carries `resetAt` — the start of the next day in UTC+3 — and `hint`. Research on your own BYOK key is not limited by the cap
| 503 | `FEATURE_NOT_ENABLED` | Deep research is unavailable on this platform |

Errors inside the SSE stream:

| Code | Description |
|-----|---------|
| `UPSTREAM_ERROR` | The provider returned an error during processing. No charge |
| `PROVIDER_DOES_NOT_SUPPORT_RESEARCH` | A server-side adapter guard — effectively a duplicate of the HTTP 404 code, surfacing when the catalog is out of sync |
| `INTERNAL_ERROR` | Internal server error during processing |

For the full list of common API errors, see [Errors](/docs/errors).

## Known specifics

**Streaming mode only.** There is no synchronous variant of `/v1/research` without SSE — deep research lasts 10–60 seconds and does not fit into ordinary HTTP proxy timeouts. If a client cannot work with a stream, accumulate events on an intermediary server and deliver them in a batch to the end application.

**Limit of 20 requests per minute per Bitrix24 account.** The current limit is returned in the `x-ratelimit-limit` header. This limit is lower than for [`POST /v1/search`](/docs/search/run) because a single research request occupies the provider's resources dozens of times longer than an ordinary search. The limit is shared by all API keys of the Bitrix24 account — they draw on a single counter, so spreading the load across several keys does not raise the ceiling. When the limit is exceeded, `429 RATE_LIMITED` is returned.

**The platform research engine's pricing depends on the instance.** [`GET /v1/search/providers`](/docs/search/providers) returns the cost of the `research` mode for the platform engine in the `pricing.research` field. For all BYOK providers the cost on the platform side is 0 Ꝟ — you are billed directly by the provider, on your own account with them.

**The key selection cascade differs from `/v1/search`.** In [`POST /v1/search`](/docs/search/run), when `provider` is omitted, the platform first checks the USER/PORTAL default for any provider and only then falls back to the instance default engine. In research it is different: if the `provider` field is omitted, the server **immediately substitutes the default research engine configured on the instance**, without checking USER/PORTAL defaults for other providers. If that engine has no key at all, `404 CREDENTIAL_NOT_FOUND` is returned — even if the user has a default key for `exa` or another research provider. The USER → PORTAL → PLATFORM cascade applies **within** the chosen provider. To use a different research provider, pass `provider` explicitly.

**Charging happens only after the stream completes successfully.** 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. If the stream breaks with a provider error, neither quota nor wallet funds are consumed. The payment source selection order is described under [Pricing](/docs/search#pricing).

## See also

- [Web Search for AI](/docs/search)
- [Search (POST /v1/search)](/docs/search/run)
- [Providers](/docs/search/providers)
- [Your own keys (BYOK)](/docs/search/credentials)
- [Keys and authorization](/docs/keys-auth)
- [Errors](/docs/errors)
