
## Исход выкладки

`GET /v1/infra/operations/:operationId`

Возвращает исход конкретной выкладки по её идентификатору. Нужен, когда соединение с [`POST /deploy`](./deploy.md) оборвалось и терминальный ответ до клиента не дошёл: идентификатор выдаётся до начала работы, поэтому по нему можно узнать, чем всё кончилось, вместо того чтобы запускать выкладку заново.

Ручка только на чтение, побочных действий у неё нет. Каждая выкладка адресуется отдельно, включая предыдущие попытки на том же сервере. Исход хранится 7 суток.

**Идентификатор выдаётся только при выкладке на отдельную виртуальную машину** (`kind: "STANDALONE"`). У galaxy-приложения (`kind: "GALAXY_APP"`) его нет, и сверить исход такой выкладки этой ручкой пока нельзя.

Где взять идентификатор — три канала, все три несёт одна и та же выкладка:

- заголовок ответа `X-Vibe-Operation-Id` — приходит сразу, до начала долгой работы;
- первый кадр `event: operation` при `?stream=true` — браузерный `EventSource` заголовков ответа не отдаёт, поэтому для него это единственный доступный канал;
- поле `operationId` в теле — `data.operationId` при успехе, `error.operationId` при отказе.

**Отсутствие идентификатора не означает, что выкладка не запускалась.** Если платформа не смогла завести запись, выкладка продолжается без идентификатора, а заголовка, кадра и поля не будет. Читать это как отказ и повторять выкладку нельзя — второй запуск ляжет поверх приложения, которое уже могло подняться.

## Параметры

| Параметр | В | Тип | Обяз. | Описание |
|----------|---|-----|:-----:|----------|
| `operationId` | path | string | да | Идентификатор операции из ответа [`POST /deploy`](./deploy.md) |

## Примеры

### curl — личный ключ

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.tech/v1/infra/operations/OPERATION_ID
```

### curl — OAuth-приложение

```bash
curl -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  https://vibecode.bitrix24.tech/v1/infra/operations/OPERATION_ID
```

### JavaScript — личный ключ

```javascript
// Выкладка: забираем идентификатор из заголовка ДО того, как придёт тело
const deploy = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/deploy`,
  {
    method: 'POST',
    headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
    body: JSON.stringify({ source: { content: archiveBase64 }, start: 'node index.js' }),
  }
)
const operationId = deploy.headers.get('X-Vibe-Operation-Id')

// Связь оборвалась — спрашиваем исход вместо повторной выкладки
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/operations/${operationId}`,
  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)

if (res.status === 410) {
  console.log('Операция была, исход больше не хранится')
} else if (res.status === 404) {
  console.log('Такой операции нет')
} else {
  const { data } = await res.json()
  console.log(`Статус: ${data.status}, шаг: ${data.step}`)
}
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.tech/v1/infra/operations/${operationId}`,
  {
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.operationId` | string | Идентификатор операции |
| `data.kind` | string | Вид операции. Сейчас всегда `deploy` |
| `data.serverId` | string | ID сервера, на котором шла выкладка |
| `data.status` | string | `running`, `succeeded`, `failed` или `unknown` — см. таблицу ниже |
| `data.step` | string \| null | Шаг конвейера, на котором операция находится или на котором остановилась |
| `data.startedAt` | string | Время старта, ISO 8601 |
| `data.finishedAt` | string \| null | Время завершения, ISO 8601. `null`, пока операция идёт |
| `data.error` | object \| null | `{ code, message }` при `status: "failed"`, иначе `null` |

Значения `data.status`:

| Значение | Что означает |
|----------|--------------|
| `running` | Выкладка идёт прямо сейчас |
| `succeeded` | Выкладка завершилась успешно |
| `failed` | Выкладка провалилась, причина — в `data.error` |
| `unknown` | Процесс прервался между стартом и записью исхода, поэтому исход неизвестен. Не то же самое, что «выкладки не было»: операция точно запускалась. Текущее состояние сервера смотрите в [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) |

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

Выкладка идёт:

```json
{
  "success": true,
  "data": {
    "operationId": "clz9k2m4x0001qw8h3f7d2n5p",
    "kind": "deploy",
    "serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
    "status": "running",
    "step": "install",
    "startedAt": "2026-08-11T09:14:22.117Z",
    "finishedAt": null,
    "error": null
  }
}
```

Выкладка завершилась успешно:

```json
{
  "success": true,
  "data": {
    "operationId": "clz9k2m4x0001qw8h3f7d2n5p",
    "kind": "deploy",
    "serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
    "status": "succeeded",
    "step": null,
    "startedAt": "2026-08-11T09:14:22.117Z",
    "finishedAt": "2026-08-11T09:18:04.902Z",
    "error": null
  }
}
```

Выкладка провалилась:

```json
{
  "success": true,
  "data": {
    "operationId": "clz9k2m4x0001qw8h3f7d2n5p",
    "kind": "deploy",
    "serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
    "status": "failed",
    "step": "healthcheck",
    "startedAt": "2026-08-11T09:14:22.117Z",
    "finishedAt": "2026-08-11T09:17:41.338Z",
    "error": {
      "code": "DEPLOY_STEP_FAILED",
      "message": "connect ECONNREFUSED 127.0.0.1:3000"
    }
  }
}
```

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

410 — операция была, но исход уже не хранится:

```json
{
  "success": false,
  "error": {
    "code": "OPERATION_OUTCOME_EXPIRED",
    "message": "The outcome of this deploy is no longer stored (kept for 7 days).",
    "serverId": "8f14e45f-ceea-467a-9c8d-2f1c5a6b7e30",
    "startedAt": "2026-07-30T11:02:19.400Z"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_OPERATION_ID` | Идентификатор не той формы, которую выдаёт платформа |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 404 | `OPERATION_NOT_FOUND` | Операции нет, она принадлежит другому ключу либо запись уже удалена |
| 410 | `OPERATION_OUTCOME_EXPIRED` | Операция ваша, но её исход больше не хранится |
| 429 | `RATE_LIMITED` | Превышен лимит опроса |

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

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

- **Чужая операция отвечает так же, как несуществующая — это сделано намеренно.** Все три случая (идентификатора нет, идентификатор чужой, запись удалена) дают один и тот же `404`. Отдельный ответ на чужую операцию подтверждал бы, что такой идентификатор существует, а идентификатор сам по себе является ключом доступа к записи: подобрав его, можно было бы прочитать чужую выкладку.
- **410 отличается от 404 только для владельца.** «Исход больше не хранится» приходит, когда операция ваша и она точно была, просто прошло больше 7 суток. На чужой или выдуманный идентификатор такой ответ не приходит никогда.
- **Запись живёт дольше, чем хранится исход.** Через 7 суток исход перестаёт отдаваться, но сам факт операции сохраняется ещё некоторое время — за счёт этого и возможен различимый ответ `410`. Потом запись удаляется, и тот же идентификатор начинает отвечать `404`.
- **`unknown` — это ответ, а не ошибка.** Он означает, что процесс выкладки прервался между стартом и записью исхода: операция точно была, но чем кончилась — платформа не знает. Смотрите текущее состояние сервера и, если приложение не поднялось, выкладывайте заново.
- **Лимит опроса — 60 запросов в минуту на ключ.** Исход не меняется чаще, чем идёт сама выкладка, поэтому опрашивать раз в несколько секунд достаточно.

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

- [Выложить приложение](./deploy.md)
- [Получить сервер](/docs/infra/servers/get)
- [Логи приложения](./logs.md)
