For AI agents: markdown of this page — /docs-content-en/performan/manager-answer.md documentation index — /llms.txt
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 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 |
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
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
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
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
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.
{
"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:
{
"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.
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 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.