## Manager answer

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

Saves a manager's answers on a review card. By default this is a draft — the card status stays as it was. An `isAutosave` value of `false` finalises the review, and that cannot be undone.

## Request body fields

The method has no path or query parameters — the card is named by a body field.

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `relationId` | number | yes | Manager review card id. Taken from the `id` field of the [`GET /v1/performan/review/manager/reviews`](./manager-reviews.md) response |
| `answers` | array | no | Answers to the stage questions. Without this field the request saves the card leaving the answers unchanged |
| `answers[].questionId` | number | yes | Question id. The list — [`GET /v1/performan/review/manager/questions`](./manager-questions.md) |
| `answers[].answerText` | string | no | Answer text for a question of type `3` |
| `answers[].selectedOptions` | number[] | no | Ids of the chosen options for a question of type `1` or `2`. The limit on obtaining them is described under "Known specifics" |
| `answers[].selectedOptionsText` | object | no | Extra text for the chosen options. The key is an option id, the value is a string |
| `isAutosave` | boolean | no | `true` — save a draft, the card status does not change. `false` — finalise the review. Defaults to `true` |
| `expectedStateHash` | string | no | The `stateHash` value from the previous write on that card. On a mismatch the request is rejected with code `409` |

Apart from `relationId` no field is required: a body of `{ "relationId": 4 }` saves a draft with no answers and returns a fresh `stateHash`. Fields not listed in the table are dropped and never reach Bitrix24.

## Examples

A draft of two text answers with an optimistic lock on the state hash.

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/performan/review/manager/answers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "relationId": 4,
    "answers": [
      { "questionId": 15, "answerText": "Met the quarterly goals" },
      { "questionId": 16, "answerText": "Strong engineering fundamentals" }
    ],
    "isAutosave": true,
    "expectedStateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/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": "Met the quarterly goals" },
      { "questionId": 16, "answerText": "Strong engineering fundamentals" }
    ],
    "isAutosave": true,
    "expectedStateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/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: 'Met the quarterly goals' },
      { questionId: 16, answerText: 'Strong engineering fundamentals' },
    ],
    isAutosave: true,
    expectedStateHash: '23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790',
  }),
})

const body = await res.json()

if (res.status === 409) {
  // The card was changed in parallel: re-read it and retry with the fresh hash
  throw new Error(body.error.message)
}

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

// Put the hash from the response into expectedStateHash of the next write on this card
console.log(body.data.status, body.data.stateHash)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/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: 'Met the quarterly goals' },
      { questionId: 16, answerText: 'Strong engineering fundamentals' },
    ],
    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)
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `true` on success |
| `data` | object | The card after the write |
| `data.id` | number | Card id |
| `data.campaignId` | number | The card's campaign |
| `data.campaignStageId` | number | The campaign stage the card belongs to |
| `data.stageType` | string | Stage type. Here — `managersReview` |
| `data.revieweeUserId` | number | The employee being reviewed. Employee card — `GET /v1/users/:id` |
| `data.revieweeUserName` | string | Name of the employee being reviewed |
| `data.reviewerUserId` | number | The reviewing employee |
| `data.reviewerUserName` | string | Name of the reviewing employee |
| `data.managerUserId` | number | The reviewee's manager |
| `data.managerUserName` | string | Manager name |
| `data.status` | string | Card state after the write: `new` — the draft is saved, `completed` — the review is finalised |
| `data.rate` | number | The final rate. Derived from the value of the chosen option of the rating question |
| `data.comment` | string | Card comment |
| `data.answers` | array | The card answers after the write |
| `data.answers[].questionId` | number | Question id |
| `data.answers[].questionTitle` | string | Question wording |
| `data.answers[].questionDescription` | string | Question note |
| `data.answers[].questionType` | string | Question type as a string: `1` — single choice, `2` — multiple choice, `3` — text answer |
| `data.answers[].questionTypeLabel` | string | Question type name in the portal language |
| `data.answers[].isRating` | boolean | Whether the answer forms the card's final rate |
| `data.answers[].answerText` | string | Answer text |
| `data.answers[].selectedOptions` | array | The chosen options as objects. In a request the same field takes an array of numbers |
| `data.answers[].selectedOptions[].id` | number | Option id |
| `data.answers[].selectedOptions[].text` | string | Option text |
| `data.answers[].selectedOptions[].value` | number | Numeric option value |
| `data.ratingScale` | array | The rating scale of the stage |
| `data.ratingScale[].value` | number | Scale value |
| `data.ratingScale[].label` | string | Value caption |
| `data.stateHash` | string | The card state hash. Goes into the `expectedStateHash` field of the next write |

## Response example

The draft is saved and the card status stayed `new`.

```json
{
  "success": true,
  "data": {
    "id": 4,
    "campaignId": 1,
    "campaignStageId": 5,
    "stageType": "managersReview",
    "revieweeUserId": 42,
    "revieweeUserName": "Mary Jones",
    "reviewerUserId": 7,
    "reviewerUserName": "John Smith",
    "managerUserId": 7,
    "managerUserName": "John Smith",
    "status": "new",
    "rate": 0,
    "comment": "",
    "answers": [
      {
        "questionId": 15,
        "questionTitle": "Key results and goal delivery",
        "questionDescription": "",
        "questionType": "3",
        "questionTypeLabel": "Text answer",
        "isRating": false,
        "answerText": "Met the quarterly goals",
        "selectedOptions": []
      }
    ],
    "ratingScale": [
      { "value": 1, "label": "Well below expectations" },
      { "value": 2, "label": "Below expectations" },
      { "value": 3, "label": "Meets expectations" },
      { "value": 4, "label": "Above expectations" },
      { "value": 5, "label": "Well above expectations" }
    ],
    "stateHash": "23fb2c21af87e3f692789a9082d89b89be1333492c40ddb13245cb2d9d6ec790"
  }
}
```

## Error response example

409 — the card changed since it was read:

```json
{
  "success": false,
  "error": {
    "code": "PERFORMAN_STATE_CONFLICT",
    "message": "The review changed since it was read. Re-read the relation and retry with the stateHash from the fresh response."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|------|-------------|
| 400 | `INVALID_PARAMS` | The body carries no `relationId`, or it is not a positive integer |
| 400 | `INVALID_PARAMS` | The body shape is wrong: `answers` is not an array, an element is not an object, `questionId` is not a positive integer, `selectedOptions` is not an array of ids, `answerText` is not a string, `selectedOptionsText` is not a map of an id to a string, `isAutosave` is not a boolean, `expectedStateHash` is an empty string |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not sent |
| 401 | `TOKEN_MISSING` | The key has no portal tokens. An OAuth application key requires the `Authorization: Bearer` header |
| 403 | `SCOPE_DENIED` | The key has no `performan` scope |
| 404 | `ROUTE_NOT_FOUND` | The Performance Review section is not enabled for the account. The answer is indistinguishable from the answer to any address that does not exist |
| 404 | `ENTITY_NOT_FOUND` | The section is enabled, but the account has no Performance Review module: Bitrix24 answers that the `performan.review.*` method was not found |
| 409 | `PERFORMAN_SCOPE_JUST_GRANTED` | A Cowork key was granted the `performan` scope by this very request. Repeat it and it goes through |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is in read-only mode |
| 403 | `BITRIX_ACCESS_DENIED` | The card belongs to another reviewer, or no card with that `relationId` exists. Bitrix24 answers both cases the same way |
| 409 | `PERFORMAN_STATE_CONFLICT` | The `expectedStateHash` sent does not match the current card state |
| 422 | `BITRIX_ERROR` | Bitrix24 rejected the request: the question does not belong to the card's stage, mandatory answers are missing on finalisation. The per-field breakdown is in the `error.validation` array, the machine code from Bitrix24 in `error.b24Code` |
| 429 | `RATE_LIMITED` | The request rate on the Bitrix24 side was exceeded |
| 429 | `QUEUE_OVERFLOW`, `QUEUE_TIMEOUT` | The portal request queue is full or the request did not get through the queue in time. The `Retry-After` header suggests the delay |
| 503 | `BITRIX_TIMEOUT` | Bitrix24 accepted the request but did not answer in time. Re-read the card before retrying — the write may have been applied |
| 502 | `BITRIX_UNAVAILABLE` | Bitrix24 is unavailable |

The full list of common API errors — [Errors](/docs/errors).

## Known specifics

**The default is a draft, not a finalised review.** A request without the `isAutosave` field behaves as `isAutosave: true`: the card status does not change and no finalisation side effects run. A value of `false` moves the card to `completed`, sends a notification and starts the following campaign stages. The API cannot bring a card back to the draft state.

**A choice question cannot be answered through the API.** The `answers[].selectedOptions` field takes option ids, but there is nowhere to get them: the [question list](./manager-questions.md) arrives without its options, and a card's `answers[].selectedOptions` carries only the options already chosen. So a call with `isAutosave: false` on a card whose mandatory rating question was not filled in the Bitrix24 interface returns `422 BITRIX_ERROR` with a message about unanswered mandatory fields.

**The `selectedOptions` field has a different shape in the request and in the response.** In the request body it is an array of numbers, the option ids. In the response it is an array of objects with the `id`, `text` and `value` fields. Sending a response back without converting it will not work.

**The card is named by the body's `relationId` alone.** Its id is not in the path: the method address is the same for every card, so a request without `relationId` is refused up front and never reaches Bitrix24.

**An element of the `error.validation` array does not always carry a `field` key.** A refusal on a specific field arrives with the `message` and `field` keys, while a refusal on finalisation because of unanswered mandatory questions carries `message` alone. Read `field` behind a presence check.

## See also

- [Manager review cards](/docs/performan/manager-reviews)
- [Manager-stage questions](/docs/performan/manager-questions)
- [Performance review operations](/docs/performan/endpoints)
- [Errors](/docs/errors)
