## Отправить результат долгого метода

`POST /v1/solution-calls/:callId/result`

Принимает от решения исход вызова долгого метода по контракту и сам доставляет его на портал Битрикс24. Адрес этого маршрута, одноразовый токен ответа и срок решение получает заголовками вместе с самим вызовом, поэтому ни ключ платформы, ни отдельная настройка ему не нужны.

## Как приходит долгий вызов

Метод контракта с `x-vibe-async: true` платформа вызывает так же, как обычный: HTTP-запросом на маршрут решения из контракта, по туннелю приложения. Долгий вызов отличают пять заголовков:

| Заголовок | Что несёт |
|-----------|-----------|
| `Prefer` | `respond-async` — платформа не ждёт результат в ответе на этот запрос |
| `X-Vibe-Call-Id` | Идентификатор вызова вида `call_…`. Он же стоит в адресе ответа |
| `X-Vibe-Reply-Url` | Готовый адрес этого маршрута с подставленным `callId`: `https://vibecode.bitrix24.tech/v1/solution-calls/call_…/result`. Берите адрес из заголовка, а не собирайте сами |
| `X-Vibe-Reply-Token` | Токен ответа вида `vcr_…` — единственный способ аутентифицировать результат. Выпускается на один вызов |
| `X-Vibe-Reply-Deadline` | Срок в формате RFC 3339, например `2026-09-21T10:10:00.000Z`. После него вызов закрывается как `expired`, и результат уже не принимается |

Общие заголовки вызова по контракту приходят и здесь: `X-Vibe-Request-Id`, `X-Vibe-Caller-Kind` со значением `contract`, `X-Vibe-Caller-Portal-Id`, `X-Vibe-Portal-User-Id`, `X-Vibe-Operation-Id`, `X-Vibe-Contract-Version` и `Idempotency-Key`. Заголовок `X-Vibe-Invocation-Context` есть только у вызова с переданным контекстом. Заголовков `X-Vibe-Caller-Key-Id` и `X-Vibe-Caller-App-Id` у вызова по контракту нет — он идёт не по ключу внешнего API.

Ответить на такой вызов решение может двумя способами:

| Ответ решения | Что происходит |
|---------------|----------------|
| `202` | Платформа считает вызов принятым и ждёт результат на этом маршруте до срока из `X-Vibe-Reply-Deadline`. Тело ответа не читается |
| `200` с телом по форме этого маршрута | Результат принимается сразу, без отдельного запроса: тело проходит ту же проверку, что и на этом маршруте, и вызов закрывается исходом `completed` либо `failed` |

Ответ `4xx` закрывает вызов как неуспешный без результата — присылать его потом бессмысленно. Остальные ответы — `5xx`, `3xx`, `2xx` кроме `200` и `202` — вместе с обрывом соединения и таймаутом платформа считает временным сбоем и повторяет доставку с нарастающей паузой до срока из `X-Vibe-Reply-Deadline` — с тем же `X-Vibe-Call-Id` и тем же токеном ответа, так что результат, посчитанный по первой попытке, этот маршрут принимает без оговорок.

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|----------|
| `callId` (path) | string | да | Идентификатор вызова из `X-Vibe-Call-Id`. Проще взять готовый адрес из `X-Vibe-Reply-Url` |

Аутентификация — заголовок `Authorization: Bearer` с токеном ответа из `X-Vibe-Reply-Token` того же вызова. Ключ платформы `vibe_…` на этом маршруте не принимается, а токен ответа не открывает ни одного другого адреса.

## Поля запроса (body)

Тело — JSON с одним из двух исходов. Размер тела — до 512 КиБ, считается по сырым байтам до разбора.

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `outcome` | string | да | `completed` — метод выполнен, `failed` — метод завершился ошибкой |
| `result` | object | при `completed` | Результат по схеме `callbacks.result` той версии контракта, по которой вызов принят. Публикация новой версии после вызова на проверку не влияет |
| `error` | object | нет | При `failed` — описание ошибки. Допустим и пустой объект: вызов всё равно закрывается как `failed` |
| `error.code` | string | нет | Код ошибки решения по шаблону `[A-Z0-9_]{1,64}`. Значение вне шаблона отбрасывается |
| `error.message` | string | нет | Текст для человека, до 500 символов. Лишнее обрезается |
| `error.problem` | object | нет | Подробности по RFC 9457: `type`, `title`, `detail`, `instance`, каждое до 500 символов. Другие ключи отбрасываются |

Тело при `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"
    }
  }
}
```

## Примеры

Ось авторизации у маршрута одна — токен ответа из `X-Vibe-Reply-Token`, поэтому пример один на язык: личный ключ и ключ авторизации приложения получают `401 REPLY_TOKEN_INVALID`.

### curl — токен ответа

```bash
curl -X POST "https://vibecode.bitrix24.tech/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 — токен ответа

```javascript
// Заголовки входящего вызова — из обработчика долгого метода в решении.
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) {
  // 4xx закрывает вызов: повторять тот же результат бессмысленно.
  console.error(`результат не принят: ${body.error.code}`);
}
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.state` | string | Состояние, в которое закрылся вызов: `completed` при `outcome: completed`, `failed` при `outcome: failed` |

## Пример ответа

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

## Пример ответа при ошибке

422 — тело больше 512 КиБ, вызов закрыт как `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
  }
}
```

Поле `error.errorCode` называет причину — `SOLUTION_RESULT_TOO_LARGE` или `SOLUTION_BAD_RESPONSE`, и с тем же кодом исход уходит на портал. Поле `error.limitBytes` приходит только при превышении размера. У `409 SOLUTION_CALL_CLOSED` вместо них поле `error.state` — состояние вызова.

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 401 | `REPLY_TOKEN_INVALID` | В `Authorization` нет `Bearer` с токеном вида `vcr_…`, в том числе когда вместо него передан ключ платформы |
| 401 | `REPLY_TOKEN_INVALID` | Токен не подходит ни к одному вызову: чужой токен, несуществующий `callId` или вызов уже удалён с платформы по сроку хранения. Существует ли `callId`, ответ не раскрывает |
| 409 | `SOLUTION_CALL_CLOSED` | Вызов уже закрыт, `error.state` несёт его состояние: `completed`, `failed`, `canceled` или `expired` |
| 409 | `SOLUTION_CALL_CLOSED` | `error.state` равен `accepted`: вызов открыт, но занят предыдущей попыткой отправить результат, которая не завершилась. Повторите позже — платформа отпускает такой вызов в пределах двух минут |
| 422 | `RESULT_INVALID` | `error.errorCode` равен `SOLUTION_RESULT_TOO_LARGE`: тело больше 512 КиБ, `error.limitBytes` — предел в байтах. Вызов закрыт как `failed` |
| 422 | `RESULT_INVALID` | `error.errorCode` равен `SOLUTION_BAD_RESPONSE`: тело не JSON, `outcome` вне `completed` и `failed`, при `completed` нет `result` либо `result` не проходит схему `callbacks.result` контракта. Вызов закрыт как `failed` |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

- **Любой `4xx` закрывает вызов окончательно.** `422` — не приглашение исправить тело и повторить: вызов уже переведён в `failed`, исход с кодом причины ушёл на портал, и тот же токен дальше отвечает `409`. Проверяйте `result` по схеме `callbacks.result` до отправки — правка кода решения поможет следующему вызову, а не этому.
- **Повторять нужно только после `5xx` и обрыва соединения.** Токен одноразов на принятый исход, а не на попытку: пока платформа не ответила `2xx` или `4xx`, результат не записан, и повтор с тем же адресом, токеном и телом обязателен — иначе шаг на портале дождётся срока и завершится по таймауту. Незавершённая попытка держит вызов одну-две минуты, и на это время ответ — `409` со `state: accepted`.
- **Токен ответа — секрет одного вызова.** Тот, у кого он есть, закроет ваш вызов любым результатом. Не пишите его в журнал, не показывайте в интерфейсе и не передавайте дальше. Платформа в свои журналы не пишет ни токен, ни тело результата.
- **После срока или отмены результат порталу не нужен.** `409` со `state: expired` или `state: canceled` — сигнал прервать долгую работу: продолжать её незачем, а её итог уже некуда принять.
- **Ответ `200` означает «исход записан», а не «портал его получил».** Доставку на портал платформа ведёт сама, с повторами при недоступности портала. Решению для этого ничего делать не нужно.

## Смотрите также

- [Внешний API приложения](/docs/applications/external-api)
- [Что приходит в приложение](/docs/infra/app-runtime)
- [Витрина приложений](/docs/applications)
- [Ошибки](/docs/errors)
