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 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 |
|---|---|---|---|---|
Fieldquery |
Typestring | Requiredyes | Default— | DescriptionResearch query text. From 1 to 2000 characters |
Fieldprovider |
Typestring | Requiredno | Defaultinstance default research engine | DescriptionA 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 |
FieldmaxSteps |
Typenumber | Requiredno | Default5 | DescriptionMaximum number of agentic-loop steps. From 1 to 10. The adapter caps the value if the external provider does not accept that many steps |
Fieldlang |
Typestring | Requiredno | Defaultauto |
DescriptionResearch language: ru, en, or auto |
FieldincludeDomains |
Typestring[] | Requiredno | Default[] |
DescriptionSearch 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 |
FieldexcludeDomains |
Typestring[] | Requiredno | Default[] |
DescriptionExclude 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 application
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 application
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 |
|---|---|---|
Eventstart |
When it occursAt the start of the request | data fieldsresearch_id, provider |
Eventthinking |
When it occursIntermediate agent reasoning | data fieldscontent — a text fragment |
Eventtool_call |
When it occursThe agent invoked an internal tool | data fieldstool (web_search / content_extraction / external_tool), description |
Eventtool_result |
When it occursA tool returned a result | data fieldsdescription, items_count |
Eventsection_complete |
When it occursA research section is finished, with its sources attached | data fieldssection.title, section.content, section.sourceIds |
Eventanswer_delta |
When it occursThe next fragment of the final answer | data fieldscontent — a chunk of text |
Eventdone |
When it occursFinal block | data fieldssee the "Fields of the done event" table below |
Eventerror |
When it occursFailure during processing | data fieldserror.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 |
|---|---|---|
Fieldquery |
Typestring | DescriptionThe original query text |
Fieldprovider |
Typestring | DescriptionIdentifier of the provider that processed the request (for example, tavily, exa) |
Fieldsynthesized_answer |
Typestring | DescriptionThe full synthesized research answer |
Fieldoutline |
Typeobject | null | DescriptionThe structure of the report's sections. null if the provider does not return a breakdown |
Fieldoutline.sections |
Typearray | DescriptionArray of sections |
Fieldoutline.sections[].title |
Typestring | DescriptionSection title |
Fieldoutline.sections[].content |
Typestring | DescriptionSection text |
Fieldoutline.sections[].sourceIds |
Typenumber[] | DescriptionIdentifiers of the sources from sources[] that the section relies on |
Fieldsources |
Typearray | DescriptionArray of research sources |
Fieldsources[].id |
Typenumber | DescriptionSequential source number |
Fieldsources[].url |
Typestring | DescriptionPage URL |
Fieldsources[].title |
Typestring | DescriptionPage title |
Fieldsources[].fullContent |
Typestring | DescriptionFull page text. Length is capped by the provider |
Fieldsources[].publishedDate |
Typestring | null | DescriptionPublication date in ISO 8601 format |
Fieldfollow_up_questions |
Typestring[] | null | DescriptionFollow-up questions for deepening the research. null if the provider does not return them |
Fieldreasoning |
Typearray | null | DescriptionThe agent's reasoning steps. null if the provider does not return them |
Fieldreasoning[].step |
Typenumber | DescriptionSequential step number |
Fieldreasoning[].description |
Typestring | DescriptionShort step description |
Fieldresearch_id |
Typestring | DescriptionRequest identifier in the format wr_<14 digits>_<8 hex> |
Fieldupstream_search_id |
Typestring | null | DescriptionIdentifier at the external provider. Useful for incident investigation |
Fieldbilling_source |
Typestring | DescriptionOptional. 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 |
Fieldcost_vibes |
Typenumber | DescriptionHow many Ꝟ were actually charged to the wallet. When billing_source is quota, cowork or byok, the value is 0 |
Fieldduration_ms |
Typenumber | DescriptionRequest processing duration in milliseconds |
Fieldpartial_charge |
Typeboolean | DescriptionPresent 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 |
Fieldcharge_log_failed |
Typeboolean | DescriptionOptional. 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 |
Fieldignored_filters |
Typestring[] | DescriptionOptional. 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:
{
"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 |
|---|---|---|
| HTTP400 | CodeINVALID_REQUEST |
DescriptionEmpty 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 |
| HTTP402 | CodeINSUFFICIENT_BALANCE |
DescriptionThe portal balance has insufficient Ꝟ for a paid provider. Switch to BYOK or top up the balance |
| HTTP402 | CodeBILLING_FROZEN |
DescriptionThe billing account is frozen |
| HTTP402 | CodeAI_QUOTA_EXHAUSTED |
DescriptionAI 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 |
| HTTP429 | CodeAI_PACING_LIMITED |
DescriptionThe 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 |
| HTTP402 | CodeCOWORK_QUOTA_EXHAUSTED |
DescriptionA 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 |
| HTTP402 | CodeCOWORK_SUBSCRIPTION_INACTIVE |
DescriptionThere is no active Cowork seat. Activate or resume the seat, or use your own provider key |
| HTTP402 | Codecompany_budget_exhausted |
DescriptionThe 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 |
| HTTP403 | CodeSCOPE_DENIED |
DescriptionThe key lacks the vibe:search scope |
| HTTP404 | CodePROVIDER_NOT_FOUND |
DescriptionA provider with this identifier does not exist or is unavailable |
| HTTP404 | CodeCREDENTIAL_NOT_FOUND |
DescriptionThere is no USER, PORTAL, or PLATFORM key for the provider |
| HTTP404 | CodePROVIDER_DOES_NOT_SUPPORT_RESEARCH |
DescriptionA 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 |
| HTTP429 | CodeRATE_LIMITED |
DescriptionThe 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 |
| HTTP429 | CodeSEARCH_DAILY_CAP_REACHED |
DescriptionWith 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. 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 |
| HTTP503 | CodeFEATURE_NOT_ENABLED |
DescriptionDeep research is unavailable on this platform |
Errors inside the SSE stream:
| Code | Description |
|---|---|
CodeUPSTREAM_ERROR |
DescriptionThe provider returned an error during processing. No charge |
CodePROVIDER_DOES_NOT_SUPPORT_RESEARCH |
DescriptionA server-side adapter guard — effectively a duplicate of the HTTP 404 code, surfacing when the catalog is out of sync |
CodeINTERNAL_ERROR |
DescriptionInternal server error during processing |
For the full list of common API errors, see 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 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 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.