## Ответ менеджера

`POST /v1/performan/review/manager/answers`

Сохраняет ответы менеджера по карточке ревью. По умолчанию это черновик — статус карточки остаётся прежним. Поле `isAutosave` со значением `false` завершает оценку, и отменить это нельзя.

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

Параметров в пути и в строке запроса у метода нет — карточку называет поле тела.

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|---------|
| `relationId` | number | да | Идентификатор карточки менеджерского ревью. Берётся из поля `id` ответа [`GET /v1/performan/review/manager/reviews`](./manager-reviews.md) |
| `answers` | array | нет | Ответы по вопросам этапа. Перечисляйте все ответы, которые должны остаться на карточке |
| `answers[].questionId` | number | да | Идентификатор вопроса. Список — [`GET /v1/performan/review/manager/questions`](./manager-questions.md) |
| `answers[].answerText` | string | нет | Текст ответа для вопроса с типом `3` |
| `answers[].selectedOptions` | number[] | нет | Идентификаторы выбранных вариантов для вопроса с типом `1` или `2`. Ограничение на их получение описано в «Известных особенностях» |
| `answers[].selectedOptionsText` | object | нет | Дополнительный текст к выбранным вариантам. Ключ — идентификатор варианта, значение — строка |
| `isAutosave` | boolean | нет | `true` — сохранить черновик, статус карточки не меняется. `false` — завершить оценку. По умолчанию `true` |
| `expectedStateHash` | string | нет | Значение `stateHash` из ответа на предыдущую запись по этой карточке. При расхождении запрос отклоняется кодом `409` |

Кроме `relationId` обязательных полей нет. Поле `answers` доходит до Битрикс24 при каждой записи — тело без этого поля отправляет пустой набор ответов, поэтому перечисляйте в `answers` все ответы, которые должны остаться на карточке. Поля, не перечисленные в таблице, отбрасываются и до Битрикс24 не доходят.

## Примеры

Черновик двух текстовых ответов с оптимистичной блокировкой по хэшу состояния.

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

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/performan/review/manager/answers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationId": 4,
    "answers": [
      { "questionId": 15, "answerText": "Закрыл цели квартала" },
      { "questionId": 16, "answerText": "Сильная инженерная база" }
    ],
    "isAutosave": true,
    "expectedStateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }'
```

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

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/performan/review/manager/answers \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "relationId": 4,
    "answers": [
      { "questionId": 15, "answerText": "Закрыл цели квартала" },
      { "questionId": 16, "answerText": "Сильная инженерная база" }
    ],
    "isAutosave": true,
    "expectedStateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }'
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/performan/review/manager/answers', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    relationId: 4,
    answers: [
      { questionId: 15, answerText: 'Закрыл цели квартала' },
      { questionId: 16, answerText: 'Сильная инженерная база' },
    ],
    isAutosave: true,
    expectedStateHash: '23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790',
  }),
})

const body = await res.json()

if (res.status === 409) {
  // Карточку изменили параллельно: перечитать и повторить со свежим хэшем
  throw new Error(body.error.message)
}

if (!body.success) {
  throw new Error(`${body.error.code}: ${body.error.message}`)
}

// Хэш из ответа кладём в expectedStateHash следующей записи по этой карточке
console.log(body.data.status, body.data.stateHash)
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/performan/review/manager/answers', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    relationId: 4,
    answers: [
      { questionId: 15, answerText: 'Закрыл цели квартала' },
      { questionId: 16, answerText: 'Сильная инженерная база' },
    ],
    isAutosave: true,
    expectedStateHash: '23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790',
  }),
})

const body = await res.json()

if (!body.success) {
  throw new Error(`${body.error.code}: ${body.error.message}`)
}

console.log(body.data.status, body.data.stateHash)
```

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

| Поле | Тип | Описание |
|------|-----|---------|
| `success` | boolean | Всегда `true` при успехе |
| `data` | object | Карточка после записи |
| `data.id` | number | Идентификатор карточки |
| `data.campaignId` | number | Кампания карточки |
| `data.campaignStageId` | number | Этап кампании, к которому относится карточка |
| `data.stageType` | string | Тип этапа. Здесь — `managersReview` |
| `data.revieweeUserId` | number | Оцениваемый сотрудник. Карточка сотрудника — `GET /v1/users/:id` |
| `data.revieweeUserName` | string | Имя оцениваемого сотрудника |
| `data.reviewerUserId` | number | Оценивающий сотрудник |
| `data.reviewerUserName` | string | Имя оценивающего сотрудника |
| `data.managerUserId` | number | Руководитель оцениваемого |
| `data.managerUserName` | string | Имя руководителя |
| `data.status` | string | Состояние карточки после записи: `new` — черновик сохранён, `completed` — оценка завершена |
| `data.rate` | number | Итоговая оценка. Выводится из значения выбранного варианта рейтингового вопроса |
| `data.comment` | string | Комментарий к карточке |
| `data.answers` | array | Ответы карточки после записи |
| `data.answers[].questionId` | number | Идентификатор вопроса |
| `data.answers[].questionTitle` | string | Формулировка вопроса |
| `data.answers[].questionDescription` | string | Пояснение к вопросу |
| `data.answers[].questionType` | string | Тип вопроса строкой: `1` — одиночный выбор, `2` — множественный выбор, `3` — текстовый ответ |
| `data.answers[].questionTypeLabel` | string | Название типа вопроса на языке портала |
| `data.answers[].isRating` | boolean | Формирует ли ответ итоговую оценку карточки |
| `data.answers[].answerText` | string | Текст ответа |
| `data.answers[].selectedOptions` | array | Выбранные варианты объектами. В запросе то же поле принимает массив чисел |
| `data.answers[].selectedOptions[].id` | number | Идентификатор варианта |
| `data.answers[].selectedOptions[].text` | string | Текст варианта |
| `data.answers[].selectedOptions[].value` | number | Числовое значение варианта |
| `data.ratingScale` | array | Шкала оценки этапа |
| `data.ratingScale[].value` | number | Значение шкалы |
| `data.ratingScale[].label` | string | Подпись значения |
| `data.stateHash` | string | Хэш состояния карточки. Передаётся в поле `expectedStateHash` следующей записи |

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

Черновик сохранён, статус карточки остался `new`.

```json
{
  "success": true,
  "data": {
    "id": 4,
    "campaignId": 1,
    "campaignStageId": 5,
    "stageType": "managersReview",
    "revieweeUserId": 42,
    "revieweeUserName": "Мария Иванова",
    "reviewerUserId": 7,
    "reviewerUserName": "Иван Петров",
    "managerUserId": 7,
    "managerUserName": "Иван Петров",
    "status": "new",
    "rate": 0,
    "comment": "",
    "answers": [
      {
        "questionId": 15,
        "questionTitle": "Ключевые результаты и выполнение целей",
        "questionDescription": "",
        "questionType": "3",
        "questionTypeLabel": "Текстовый ответ",
        "isRating": false,
        "answerText": "Закрыл цели квартала",
        "selectedOptions": []
      }
    ],
    "ratingScale": [
      { "value": 1, "label": "Значительно ниже ожиданий" },
      { "value": 2, "label": "Ниже ожиданий" },
      { "value": 3, "label": "Соответствует ожиданиям" },
      { "value": 4, "label": "Выше ожиданий" },
      { "value": 5, "label": "Значительно выше ожиданий" }
    ],
    "stateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }
}
```

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

409 — карточка изменилась с момента чтения:

```json
{
  "success": false,
  "error": {
    "code": "PERFORMAN_STATE_CONFLICT",
    "message": "The relation changed since your `expectedStateHash` — re-read it and retry with the fresh stateHash"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_PARAMS` | `relationId` в теле отсутствует или не является положительным целым числом |
| 400 | `INVALID_PARAMS` | Нарушена структура тела: `answers` не массив, элемент не объект, `questionId` не положительное целое, `selectedOptions` не массив идентификаторов, `answerText` не строка, `selectedOptionsText` не карта идентификатора в строку, `isAutosave` не логическое значение, `expectedStateHash` не строка либо пустая строка, в том числе из одних пробелов. Значение `null` в `expectedStateHash` равнозначно отсутствию поля |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `TOKEN_MISSING` | У ключа нет токенов портала. Ключ OAuth-приложения требует заголовок `Authorization: Bearer` |
| 403 | `SCOPE_DENIED` | У ключа нет скоупа `performan` |
| 404 | `ROUTE_NOT_FOUND` | Раздел «Перформанс ревью» порталу не включён. Ответ неотличим от ответа на любой несуществующий адрес |
| 404 | `ENTITY_NOT_FOUND` | Раздел включён, но модуля «Перформанс ревью» на портале нет: Битрикс24 отвечает «Метод `performan.review.*` не найден» |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ в режиме только для чтения |
| 403 | `BITRIX_ACCESS_DENIED` | Карточка принадлежит другому оценивающему либо карточки с таким `relationId` не существует. Битрикс24 отвечает на оба случая одинаково |
| 409 | `PERFORMAN_SCOPE_JUST_GRANTED` | Ключу Коворка выдан скоуп `performan` этим же запросом. Повторите запрос — он пройдёт |
| 409 | `PERFORMAN_STATE_CONFLICT` | Переданный `expectedStateHash` не совпадает с текущим состоянием карточки |
| 422 | `BITRIX_ERROR` | Битрикс24 отклонил запрос: вопрос не принадлежит этапу карточки, при завершении оценки заполнены не все обязательные ответы. Разбор по полям — в массиве `error.validation`, машинный код Битрикс24 — в `error.b24Code` |
| 429 | `RATE_LIMITED` | Превышена частота запросов на стороне Битрикс24 |
| 429 | `QUEUE_OVERFLOW`, `QUEUE_TIMEOUT` | Очередь запросов портала переполнена или запрос не дождался очереди. Заголовок `Retry-After` подсказывает задержку |
| 503 | `BITRIX_TIMEOUT` | Битрикс24 принял запрос, но не ответил за отведённое время. Перед повтором перечитайте карточку — запись могла примениться |
| 502 | `BITRIX_UNAVAILABLE` | Битрикс24 недоступен |

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

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

**По умолчанию сохраняется черновик, а не завершённая оценка.** Запрос без поля `isAutosave` работает как `isAutosave: true`: статус карточки не меняется, побочные эффекты завершения не запускаются. Значение `false` переводит карточку в `completed`, отправляет уведомление и запускает следующие этапы кампании. Вернуть карточку в состояние черновика через API нельзя.

**Ответить на вопрос с выбором варианта через API нельзя.** Поле `answers[].selectedOptions` принимает идентификаторы вариантов, но получить их неоткуда: [список вопросов](./manager-questions.md) приходит без вариантов, а в карточке в поле `answers[].selectedOptions` попадают только уже выбранные. Поэтому вызов с `isAutosave: false` на карточке, где обязательный рейтинговый вопрос не заполнен в интерфейсе Битрикс24, возвращает `422 BITRIX_ERROR` с сообщением о незаполненных обязательных полях.

**Элемент массива `error.validation` не всегда содержит поле `field`.** Отказ по конкретному полю приходит с ключами `message` и `field`, а отказ на завершении оценки по незаполненным обязательным ответам — только с `message`. Читайте `field` через проверку на наличие.

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

- [Карточки менеджерского ревью](/docs/performan/manager-reviews)
- [Вопросы менеджерского этапа](/docs/performan/manager-questions)
- [Операции перформанс-ревью](/docs/performan/endpoints)
- [Ошибки](/docs/errors)
