## 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`:

```json
{
  "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

```bash
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

```javascript
// 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

```json
{
  "success": true,
  "data": {
    "state": "completed"
  }
}
```

## Error response example

422 — the body is over 512 KiB, the call is closed as `failed`:

```json
{
  "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](/docs/errors).

## Known specifics

- **Any `4xx` closes the call for good.** A `422` is not an invitation to fix the body and retry: the call has already moved to `failed`, the outcome with the reason code has gone to the Bitrix24 account, and the same token answers `409` from then on. Validate `result` against the `callbacks.result` schema before sending — a fix in the solution's code helps the next call, not this one.
- **Retry only after a `5xx` or a dropped connection.** The token is single-use per accepted outcome, not per attempt: until the platform has answered `2xx` or `4xx`, 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 is `409` with `state: 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 `409` with `state: expired` or `state: canceled` is the signal to stop the long-running work: there is no point continuing it, and there is nowhere left to accept its outcome.
- **A `200` means "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.

## See also

- [Application external API](/docs/applications/external-api)
- [What the app receives](/docs/infra/app-runtime)
- [Application catalog](/docs/applications)
- [Errors](/docs/errors)
