لوكلاء AI: ملف markdown لهذه الصفحة — /docs-content-en/search/research.md فهرس التوثيق — /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 |
Fieldcost_vibes |
Typenumber | DescriptionHow many Ꝟ were actually charged |
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 | DescriptionPresent when the usage log could not be written. The result is returned, but no funds are charged (cost_vibes: 0) |
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","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 | 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 only on a paid call. 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 exact value arrives in the x-ratelimit-limit header (the cap is divided across replicas) |
| HTTP429 | CodeSEARCH_DAILY_CAP_REACHED |
DescriptionWith this request, the Bitrix24 account's paid search spend for the current day in UTC+3 would exceed the cap set by the Bitrix24 account admin. The rejection arrives 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 |
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. The exact value arrives in the x-ratelimit-limit header (the cap is divided across replicas). 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. 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.