For AI agents: markdown of this page — /docs-content-en/applications/solution-call-result.md documentation index — /llms.txt
Documentation articles are currently available in English.
Deliver the result of a long-running method
POST /v1/solution-calls/:callId/result
Accepts the outcome of a long-running contract method call from the solution and itself delivers it to the Bitrix24 account. The solution receives the address of this route, a single-use reply token and the deadline as headers on the call itself, so it needs neither a platform key nor any extra setup.
How a long-running call arrives
The platform invokes a contract method with x-vibe-async: true the same way as a regular one: an HTTP request to the solution's route from the contract, through the application tunnel. Five headers set a long-running call apart:
| Header | What it carries |
|---|---|
Prefer |
respond-async — the platform does not wait for the result in the response to this request |
X-Vibe-Call-Id |
Call id of the form call_…. The same id is part of the reply address |
X-Vibe-Reply-Url |
The ready-made address of this route with the callId filled in: https://vibecode.bitrix24.com/v1/solution-calls/call_…/result. Take the address from the header instead of building it yourself |
X-Vibe-Reply-Token |
Reply token of the form vcr_… — the only way to authenticate the result. Issued for one call |
X-Vibe-Reply-Deadline |
Deadline in RFC 3339 format, for example 2026-09-21T10:10:00.000Z. After it the call closes as expired and the result is no longer accepted |
The common contract-call headers arrive here as well: X-Vibe-Request-Id, X-Vibe-Caller-Kind with the value contract, X-Vibe-Caller-Portal-Id, X-Vibe-Portal-User-Id, X-Vibe-Operation-Id, X-Vibe-Contract-Version and Idempotency-Key. The X-Vibe-Invocation-Context header is present only on a call that was made with a context. A contract call carries no X-Vibe-Caller-Key-Id or X-Vibe-Caller-App-Id — it does not go through an external API key.
The solution can answer such a call in two ways:
| Solution response | What happens |
|---|---|
202 |
The platform treats the call as accepted and waits for the result on this route until the deadline from X-Vibe-Reply-Deadline. The response body is not read |
200 with a body in the form of this route |
The result is accepted right away, without a separate request: the body goes through the same validation as on this route, and the call closes with the outcome completed or failed |
A 4xx response closes the call as failed without a result — sending one later is pointless. Every other response — 5xx, 3xx, 2xx other than 200 and 202 — together with a dropped connection and a timeout the platform treats as a transient failure and retries the delivery with growing pauses until the deadline from X-Vibe-Reply-Deadline — with the same X-Vibe-Call-Id and the same reply token, so this route accepts a result computed on the first attempt without reservations.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
callId (path) |
string | yes | Call id from X-Vibe-Call-Id. It is simpler to take the ready-made address from X-Vibe-Reply-Url |
Authentication is the Authorization: Bearer header with the reply token from X-Vibe-Reply-Token of the same call. A platform key vibe_… is not accepted on this route, and the reply token opens no other address.
Request fields (body)
The body is JSON with one of two outcomes. The body size is up to 512 KiB, measured in raw bytes before parsing.
| Field | Type | Required | Description |
|---|---|---|---|
outcome |
string | yes | completed — the method succeeded, failed — the method ended with an error |
result |
object | with completed |
The result per the callbacks.result schema of the contract version the call was accepted under. Publishing a new version after the call does not affect the validation |
error |
object | no | With failed — the error description. An empty object is allowed too: the call still closes as failed |
error.code |
string | no | The solution's error code matching [A-Z0-9_]{1,64}. A value off the pattern is dropped |
error.message |
string | no | Human-readable text, up to 500 characters. The excess is cut off |
error.problem |
object | no | Details per RFC 9457: type, title, detail, instance, each up to 500 characters. Other keys are dropped |
Body with failed:
{
"outcome": "failed",
"error": {
"code": "INVOICE_LOCKED",
"message": "Invoice is locked by another user",
"problem": {
"type": "https://example.com/problems/invoice-locked",
"title": "Invoice locked",
"detail": "Invoice 17 is being edited",
"instance": "/invoices/17"
}
}
}
Examples
The route has a single authorization axis — the reply token from X-Vibe-Reply-Token, so there is one example per language: a personal key and an application authorization key get 401 REPLY_TOKEN_INVALID.
curl — reply token
curl -X POST "https://vibecode.bitrix24.com/v1/solution-calls/call_01M31PGZ80V9GJMWJ5N48AATB2/result" \
-H "Authorization: Bearer YOUR_REPLY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"outcome": "completed", "result": {"invoiceNumber": "A-17"}}'
JavaScript — reply token
// Headers of the incoming call — from the long-running method handler in the solution.
const replyUrl = req.headers['x-vibe-reply-url'];
const replyToken = req.headers['x-vibe-reply-token'];
const res = await fetch(replyUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${replyToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ outcome: 'completed', result: { invoiceNumber: 'A-17' } }),
});
const body = await res.json();
if (!body.success) {
// A 4xx closes the call: retrying the same result is pointless.
console.error(`result not accepted: ${body.error.code}`);
}
Response fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true on success |
data.state |
string | The state the call closed with: completed for outcome: completed, failed for outcome: failed |
Response example
{
"success": true,
"data": {
"state": "completed"
}
}
Error response example
422 — the body is over 512 KiB, the call is closed as failed:
{
"success": false,
"error": {
"code": "RESULT_INVALID",
"message": "The result body is over the boundary-contract limit; the call is closed as failed",
"errorCode": "SOLUTION_RESULT_TOO_LARGE",
"limitBytes": 524288
}
}
The error.errorCode field names the reason — SOLUTION_RESULT_TOO_LARGE or SOLUTION_BAD_RESPONSE — and the outcome reaches the Bitrix24 account with the same code. The error.limitBytes field comes only when the size is exceeded. A 409 SOLUTION_CALL_CLOSED carries the error.state field instead — the call state.
Errors
| HTTP | Code | Description |
|---|---|---|
| 401 | REPLY_TOKEN_INVALID |
Authorization has no Bearer with a token of the form vcr_…, including when a platform key is passed instead |
| 401 | REPLY_TOKEN_INVALID |
The token matches no call: a foreign token, a non-existent callId, or the call has already been removed from the platform by its retention period. Whether the callId exists is not disclosed |
| 409 | SOLUTION_CALL_CLOSED |
The call is already closed; error.state carries its state: completed, failed, canceled or expired |
| 409 | SOLUTION_CALL_CLOSED |
error.state is accepted: the call is open but held by a previous attempt to deliver the result that did not finish. Retry later — the platform releases such a call within two minutes |
| 422 | RESULT_INVALID |
error.errorCode is SOLUTION_RESULT_TOO_LARGE: the body is over 512 KiB, error.limitBytes is the limit in bytes. The call is closed as failed |
| 422 | RESULT_INVALID |
error.errorCode is SOLUTION_BAD_RESPONSE: the body is not JSON, outcome is neither completed nor failed, result is missing with completed, or result does not pass the contract's callbacks.result schema. The call is closed as failed |
Full list of common API errors — Errors.
Known specifics
- Any
4xxcloses the call for good. A422is not an invitation to fix the body and retry: the call has already moved tofailed, the outcome with the reason code has gone to the Bitrix24 account, and the same token answers409from then on. Validateresultagainst thecallbacks.resultschema before sending — a fix in the solution's code helps the next call, not this one. - Retry only after a
5xxor a dropped connection. The token is single-use per accepted outcome, not per attempt: until the platform has answered2xxor4xx, the result is not recorded, and a retry with the same address, token and body is mandatory — otherwise the step on the Bitrix24 account waits out the deadline and ends with a timeout. An unfinished attempt holds the call for one to two minutes, and during that time the answer is409withstate: accepted. - The reply token is the secret of one call. Whoever holds it can close your call with any result. Do not write it to a log, do not show it in the interface and do not pass it on. The platform writes neither the token nor the result body to its own logs.
- After the deadline or a cancellation the Bitrix24 account no longer needs the result. A
409withstate: expiredorstate: canceledis the signal to stop the long-running work: there is no point continuing it, and there is nowhere left to accept its outcome. - A
200means "the outcome is recorded", not "the Bitrix24 account has received it". The platform handles the delivery to the Bitrix24 account itself, with retries while the account is unavailable. The solution does not need to do anything for that.