## Сохранить чек-лист задачи

`POST /v1/tasks/:taskId/checklist/tree`

Сохраняет весь чек-лист задачи одним вызовом: переданный плоский массив пунктов заменяет текущий чек-лист. Поля пунктов пишутся в верхнем регистре.

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|----------|
| `taskId` (path) | number | да | ID задачи, целое положительное число. Список: [`GET /v1/tasks`](/docs/entities/tasks/list) |

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

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `items` | array | да | Все пункты чек-листа одним плоским массивом. Пункт, которого нет в массиве, удаляется. Пустой массив удаляет весь чек-лист. Порядок пунктов с одним родителем в массиве задаёт их `sortIndex` |
| `items[].ID` | number | нет | ID существующего пункта. Список: [`GET /v1/tasks/:taskId/checklist`](/docs/entities/tasks/checklist/list). С `ID` пункт сохраняется под прежним ID. Без него пункт создаётся заново с новым ID, а прежний удаляется |
| `items[].NODE_ID` | string | нет | Ключ пункта внутри запроса, например `root-1`. На него ссылаются подпункты в `PARENT_NODE_ID`, под ним пункт приходит в ответе. Задавайте каждому пункту свой `NODE_ID`: из нескольких новых пунктов без него сохраняется только последний, остальные пропадают без ошибки |
| `items[].PARENT_NODE_ID` | string \| number | нет | `NODE_ID` родительского пункта. `0` или без поля — пункт верхнего уровня, его `TITLE` становится названием чек-листа |
| `items[].TITLE` | string | да | Текст пункта. Нужен каждому пункту, в том числе пункту с `ID` |
| `items[].IS_COMPLETE` | boolean | нет | `true` — пункт выполнен. Без поля новый пункт сохраняется невыполненным. Строка `"Y"` вместо `true` сохраняет пункт невыполненным, ошибки при этом нет |
| `items[].IS_IMPORTANT` | boolean | нет | `true` — пункт важный. Без поля — `false`: у пункта с `ID` прежняя отметка важности снимается |
| `parameters` | object | нет | Дополнительные параметры сохранения. На результат не влияют |

## Примеры

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/tasks/4218/checklist/tree" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "NODE_ID": "root-1", "PARENT_NODE_ID": 0, "TITLE": "Подготовка к запуску", "IS_COMPLETE": false, "IS_IMPORTANT": false },
      { "NODE_ID": "child-1", "PARENT_NODE_ID": "root-1", "TITLE": "Проверить сборку", "IS_COMPLETE": false, "IS_IMPORTANT": true },
      { "NODE_ID": "child-2", "PARENT_NODE_ID": "root-1", "TITLE": "Обновить описание", "IS_COMPLETE": false, "IS_IMPORTANT": false }
    ]
  }'
```

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

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/tasks/4218/checklist/tree" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "NODE_ID": "root-1", "PARENT_NODE_ID": 0, "TITLE": "Подготовка к запуску", "IS_COMPLETE": false, "IS_IMPORTANT": false },
      { "NODE_ID": "child-1", "PARENT_NODE_ID": "root-1", "TITLE": "Проверить сборку", "IS_COMPLETE": false, "IS_IMPORTANT": true },
      { "NODE_ID": "child-2", "PARENT_NODE_ID": "root-1", "TITLE": "Обновить описание", "IS_COMPLETE": false, "IS_IMPORTANT": false }
    ]
  }'
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/tasks/4218/checklist/tree', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: [
      { NODE_ID: 'root-1', PARENT_NODE_ID: 0, TITLE: 'Подготовка к запуску', IS_COMPLETE: false, IS_IMPORTANT: false },
      { NODE_ID: 'child-1', PARENT_NODE_ID: 'root-1', TITLE: 'Проверить сборку', IS_COMPLETE: false, IS_IMPORTANT: true },
      { NODE_ID: 'child-2', PARENT_NODE_ID: 'root-1', TITLE: 'Обновить описание', IS_COMPLETE: false, IS_IMPORTANT: false },
    ],
  }),
})

const { data } = await res.json()
const saved = data?.checkListItem?.traversedItems ?? {}
for (const [nodeId, item] of Object.entries(saved)) {
  console.log(nodeId, '→ ID пункта', item.id)
}
```

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

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/tasks/4218/checklist/tree', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    items: [
      { NODE_ID: 'root-1', PARENT_NODE_ID: 0, TITLE: 'Подготовка к запуску', IS_COMPLETE: false, IS_IMPORTANT: false },
      { NODE_ID: 'child-1', PARENT_NODE_ID: 'root-1', TITLE: 'Проверить сборку', IS_COMPLETE: false, IS_IMPORTANT: true },
      { NODE_ID: 'child-2', PARENT_NODE_ID: 'root-1', TITLE: 'Обновить описание', IS_COMPLETE: false, IS_IMPORTANT: false },
    ],
  }),
})

const { data } = await res.json()
const saved = data?.checkListItem?.traversedItems ?? {}
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true`, если запрос выполнен |
| `data.checkListItem` | object \| null | Сохранённый чек-лист. `null`, если передан пустой `items` |
| `data.checkListItem.traversedItems` | object | Сохранённые пункты. Ключ — `NODE_ID` пункта из запроса, у пункта без `NODE_ID` — `"0"` |
| `data.checkListItem.traversedItems.<NODE_ID>.id` | number | ID пункта. Пункт без `ID` в запросе получает новый ID |
| `data.checkListItem.traversedItems.<NODE_ID>.copiedId` | number \| null | ID пункта-оригинала, если пункт скопирован. Метод возвращает `null` |
| `data.checkListItem.traversedItems.<NODE_ID>.entityId` | number | ID задачи из пути |
| `data.checkListItem.traversedItems.<NODE_ID>.userId` | number | ID сотрудника, от имени которого сделан вызов |
| `data.checkListItem.traversedItems.<NODE_ID>.createdBy` | number \| null | ID автора пункта. У пункта, сохранённого по `ID`, — `null`, автора такого пункта отдаёт [`GET /v1/tasks/:taskId/checklist`](/docs/entities/tasks/checklist/list) |
| `data.checkListItem.traversedItems.<NODE_ID>.parentId` | number | ID родительского пункта. `0` — пункт верхнего уровня |
| `data.checkListItem.traversedItems.<NODE_ID>.title` | string | Текст пункта |
| `data.checkListItem.traversedItems.<NODE_ID>.sortIndex` | number | Место пункта среди пунктов с тем же родителем: чем меньше число, тем выше пункт |
| `data.checkListItem.traversedItems.<NODE_ID>.displaySortIndex` | string | Номер пункта для показа. Метод возвращает пустую строку |
| `data.checkListItem.traversedItems.<NODE_ID>.isComplete` | boolean | Выполнен ли пункт |
| `data.checkListItem.traversedItems.<NODE_ID>.isImportant` | boolean | Важный ли пункт |
| `data.checkListItem.traversedItems.<NODE_ID>.completedCount` | number | Число выполненных подпунктов. Метод возвращает `0`, даже если подпункт выполнен |
| `data.checkListItem.traversedItems.<NODE_ID>.members` | array | Участники пункта. Без участников — пустой массив |
| `data.checkListItem.traversedItems.<NODE_ID>.attachments` | array \| object | Вложения пункта. Без вложений — пустой массив |
| `data.checkListItem.traversedItems.<NODE_ID>.nodeId` | string \| null | `NODE_ID` пункта из запроса. У пункта без `NODE_ID` — `null` |
| `data.checkListItem.taskId` | null | Всегда `null`. ID задачи — в `entityId` пунктов |

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

Чек-лист сохранён:

```json
{
  "success": true,
  "data": {
    "checkListItem": {
      "traversedItems": {
        "root-1": {
          "id": 268,
          "copiedId": null,
          "entityId": 4218,
          "userId": 1295,
          "createdBy": 1295,
          "parentId": 0,
          "title": "Подготовка к запуску",
          "sortIndex": 0,
          "displaySortIndex": "",
          "isComplete": false,
          "isImportant": false,
          "completedCount": 0,
          "members": [],
          "attachments": [],
          "nodeId": "root-1"
        },
        "child-1": {
          "id": 270,
          "copiedId": null,
          "entityId": 4218,
          "userId": 1295,
          "createdBy": 1295,
          "parentId": 268,
          "title": "Проверить сборку",
          "sortIndex": 0,
          "displaySortIndex": "",
          "isComplete": false,
          "isImportant": true,
          "completedCount": 0,
          "members": [],
          "attachments": [],
          "nodeId": "child-1"
        },
        "child-2": {
          "id": 272,
          "copiedId": null,
          "entityId": 4218,
          "userId": 1295,
          "createdBy": 1295,
          "parentId": 268,
          "title": "Обновить описание",
          "sortIndex": 1,
          "displaySortIndex": "",
          "isComplete": false,
          "isImportant": false,
          "completedCount": 0,
          "members": [],
          "attachments": [],
          "nodeId": "child-2"
        }
      },
      "taskId": null
    }
  }
}
```

Передан пустой `items`, чек-лист задачи удалён:

```json
{
  "success": true,
  "data": {
    "checkListItem": null
  }
}
```

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

422 — у нового пункта нет `TITLE`:

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_ERROR",
    "message": "Указано некорректное значение [] для поля [TITLE] в элементе [, ]",
    "b24Code": "0"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_PARAMS` | `taskId` — не целое положительное число. Сообщение `taskId must be a positive integer` |
| 400 | `INVALID_PARAMS` | Тело — не JSON-объект, например массив. Сообщение `JSON object body is required` |
| 400 | `INVALID_PARAMS` | Нет поля `items`, или `items` — не массив. Сообщение `items must be an array` |
| 400 | `INVALID_PARAMS` | `parameters` — не объект. Сообщение `parameters must be an object` |
| 401 | `MISSING_API_KEY` | Не передан `X-Api-Key` |
| 401 | `TOKEN_MISSING` | У ключа нет токенов Битрикс24 |
| 403 | `SCOPE_DENIED` | У ключа нет скоупа `task` |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ в режиме «только чтение». В `error.details.switchUrl` — адрес раздела, где режим меняется |
| 415 | `FST_ERR_CTP_INVALID_MEDIA_TYPE` | Тело с `Content-Type`, который API не разбирает, например `application/xml` |
| 422 | `BITRIX_ERROR` | У пункта нет `TITLE`, в том числе у пункта с `ID`. Сообщение на русском портале — `Указано некорректное значение [] для поля [TITLE] в элементе [283, ]`, где `283` — `ID` пункта. У нового пункта место ID в сообщении пустое: `[, ]` |
| 422 | `BITRIX_ERROR` | Сотрудник, от имени которого идёт вызов, не имеет доступа к задаче. Тот же ответ этот сотрудник получает на `taskId` несуществующей задачи. Сообщение на русском портале — `Доступ к задаче запрещен`, на портале `.com` — `Access to task was denied`. Спецификация API объявляет на этот случай `404 TASK_NOT_FOUND`, но на проверенных порталах приходит этот ответ |
| 429 | `RATE_LIMITED` | Превышена частота запросов. Действующий потолок — в заголовке `x-ratelimit-limit` |

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

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

**Ошибка не откатывает уже сохранённое.** Ответ `422` не значит, что чек-лист остался прежним. Запрос с новым названием пункта с `ID` и новым пунктом без `TITLE` возвращает `422`, а название пункта всё равно меняется. Пункты, которых не было в запросе, при этом не удаляются. После ошибки проверяйте итог через [`GET /v1/tasks/:taskId/checklist`](/docs/entities/tasks/checklist/list).

**Несуществующий `taskId` не всегда даёт ошибку.** Ответ зависит от сотрудника, от имени которого идёт вызов: одному приходит `422 BITRIX_ERROR`, другому — `200`. Во втором случае пункты сохраняются с `entityId`, равным переданному `taskId`, и не видны ни в одной задаче. Перед сохранением проверьте задачу через [`GET /v1/tasks/:id`](/docs/entities/tasks/get).

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

- [Действия над чек-листом и файл задачи](/docs/entities/tasks/checklist-actions)
- [Список пунктов чек-листа](/docs/entities/tasks/checklist/list)
- [Обновить пункт чек-листа](/docs/entities/tasks/checklist/update)
- [Удалить пункт чек-листа](/docs/entities/tasks/checklist/delete)
- [Отметить пункт выполненным вместе с подпунктами](/docs/entities/tasks/checklist-actions/complete-all)
- [Чек-лист задачи](/docs/entities/tasks/checklist)
- [Задачи](/docs/entities/tasks)
