For AI agents: markdown of this page — /docs-content-en/search/research.md documentation index — /llms.txt
Deep research
POST /v1/research
Launches a deep iterative search with 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 | Req. | 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 — 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
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 app
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
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 app
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()
Event stream (SSE)
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).
| 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 |
cost_vibes |
number | How many Ꝟ were actually charged |
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 | Present when the usage log could not be written. The result is returned, but no funds are charged (cost_vibes: 0) |
ignored_filters |
string[] | Optional. The filters that were passed but the engine could not apply. For example, includeDomains_truncated when more than 20 domains were passed and the list was truncated to 20 |
Event-stream 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":"got 8 sources","items_count":8}
event: tool_call
data: {"tool":"content_extraction","description":"reading top sources"}
event: answer_delta
data: {"content":"Cursor 3 is a reworked environment "}
event: answer_delta
data: {"content":"for AI development [1][2]. Windsurf, in turn, "}
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,"research_id":"wr_20260504112304_a1b2c3d4","upstream_search_id":"tvly-research-abc","cost_vibes":0,"duration_ms":18230}
Error response example
402 — not enough Ꝟ on the balance:
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient vibes for this research request.",
"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
}
}
Three 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. The same set is used for INSUFFICIENT_BALANCE and BILLING_FROZEN. All other cases are listed in the error table below.
Errors
Errors 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 from 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 |
| 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 |
| 429 | RATE_LIMITED |
The limit of 20 requests per minute per portal — shared by all of its API keys — was exceeded |
| 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 |
The full list of common API errors — 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. Lower than for POST /v1/search, 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 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, 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 go through a different research provider, pass provider explicitly.
Charging happens only after the stream completes successfully. The balance is checked before the run starts: if it is insufficient, 402 INSUFFICIENT_BALANCE is returned without contacting the provider. If the stream breaks with a provider error, the balance does not change — there is nothing to charge for.
See also
- Web Search for AI — section overview, pricing, supported capabilities
- Search (POST /v1/search) — synchronous and streaming search without the agentic loop
- Providers — list of engines with research support
- Your own keys (BYOK) — adding BYOK keys for research providers
- Keys and authorization — personal key and authorization key
- Errors — API error code reference