
## List comments

`GET /v1/tasks/:taskId/comments`

Returns the comments of a specific task. On the new task card the list is read from the chat; system messages are filtered out. On the old card it is read from the comments block inside the card.

Comments written before the Bitrix24 account moved to the new task card do not migrate into the chat, so a task that has a chat can still keep part of its comments in the legacy store. When the chat is read to its end and yields no comment at all, the list reads the legacy store and answers from it; when the chat yielded nothing and could not be read to its end either, the legacy store is still read, but the chat is not listed in `meta.sources`. `meta.sources` names the stores whose contents this response learned, while `meta.truncated` reports whether what was read is complete: together they tell you whether an empty `data` means that the task has no comments or that only one of the two stores was heard from.

## Parameters

| Parameter | Type | Required | Default | Description |
|----------|-----|:-----:|-----------|---------|
| `taskId` (path) | number | yes | — | Parent task ID |
| `limit` (query) | number | | `50` | Page size (up to 200) |
| `offset` (query) | number | | `0` | How many matching comments to skip before returning results. Honored on every read path, including an answer from the legacy store; a request without `filter` and with a sort by `ID` used to ignore it and return the same page at any `offset`, while an answer from the legacy store used to arrive as whatever Bitrix24 returned in one call |
| `sort` (query) | string | | `id:desc` | Sorting: field `ID`, `AUTHOR_ID` or `POST_DATE` and direction `asc` or `desc` — `?sort=post_date:desc`. The field name is case-insensitive; the camelCase names `id`, `authorId`, `authorName`, `createdAt`, `authorEmail` are accepted too. Sorting by `AUTHOR_NAME` and `AUTHOR_EMAIL` is accepted on the old card only; on the new one it returns `400 UNSUPPORTED_SORT_FIELD` |
| `filter` (query) | object | | — | Filter by the fields `ID`, `AUTHOR_ID`, `POST_DATE`. The camelCase names `id`, `authorId`, `authorName`, `createdAt` are accepted too. Passed as JSON: `?filter={"AUTHOR_ID":1}`. The field name may carry a `!`, `>`, `>=`, `<` or `<=` prefix — `?filter={">=POST_DATE":"2026-05-01T00:00:00Z"}`. The value for `POST_DATE` is an ISO 8601 date in UTC; several fields are combined with "and". Filtering by `AUTHOR_NAME` is accepted on the old card only; on the new one it returns `400 UNSUPPORTED_FILTER_FIELD`. **Important:** pass the whole filter in a single form — either bracket notation or one JSON object. A mixed envelope (`?filter[NAME]=x&filter=`) and a bracket condition deeper than two levels are rejected with `400 INVALID_FILTER`: half of the conditions are irrecoverably lost while the query string is parsed. An empty `?filter=` means "no filter". |

## Examples

### curl — personal key

```bash
curl "https://vibecode.bitrix24.com/v1/tasks/289/comments?limit=20&sort=id:desc" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl "https://vibecode.bitrix24.com/v1/tasks/289/comments?limit=20&sort=id:desc" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const url = 'https://vibecode.bitrix24.com/v1/tasks/289/comments?limit=20&sort=id:desc'
const res = await fetch(url, {
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
  },
})

const { success, data, meta } = await res.json()
console.log(`Received ${data.length} comments`)
```

### JavaScript — OAuth application

```javascript
const url = 'https://vibecode.bitrix24.com/v1/tasks/289/comments?limit=20&sort=id:desc'
const res = await fetch(url, {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

const { success, data, meta } = await res.json()
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data` | array | Array of comments |
| `data[].id` | number | Comment identifier |
| `data[].taskId` | number | Parent task ID |
| `data[].authorId` | number | Author. Profile: `GET /v1/users/:authorId` |
| `data[].message` | string | Comment text (supports BB-code) |
| `data[].createdAt` | datetime | Creation date (UTC ISO 8601) |
| `data[].attachments` | array | Attached files. The key is always present; with no files it is an empty list. Each element: `fileId`, `downloadUrl`, plus `name` and `size` when Bitrix24 returns them. Downloading — [Download an attachment](./file-download.md) |
| `meta.total` | number | Number of comments. On the new card for a request with `filter`, a sort other than `ID`, or a non-zero `offset` — the exact count of the matches within the scanned window. Without those — the exact count when `meta.hasMore` is `false`; on a full page of comments, an "at least" estimate equal to `data.length + 1`. In an answer from the legacy store it is the counter Bitrix24 returned for THIS call (scoped by `filter` when one is given); it may return no counter of its own, and the number then degenerates into the row count of the single page received. In neither case is it a task-wide counter: that store returns at most 50 records per call and offers no navigation. It may exceed the length of `data`; it cannot reach past that one call. Read it together with `meta.truncated` |
| `meta.hasMore` | boolean | Whether there are more comments beyond `limit` — within the set the answer reached. On the chat walk that set is the chat ONLY: pre-migration comments in the legacy store are not part of it, so `hasMore: false` there means the chat ended, not that the task ran out of comments. In an answer from the legacy store the set is limited to ONE `task.commentitem.getlist` call (at most 50 records, no navigation): `hasMore: false` then means "nothing left to page inside the page we got", not "the comments ended". ⚠️ So make the TRIPLE your exit condition: page while `hasMore` is `true`; the answer is complete only when no `meta.truncated` sits beside it AND `meta.sources` equals `["chat", "legacy"]` |
| `meta.truncated` | boolean | Present only with the value `true`: what was read is incomplete, comments remained beyond the edge of what the answer reached. BOTH paths send it, and `meta.total` beside it means different things. The **chat walk** (new card) stopped before the history ended — the scan window was exhausted, the walk broke off, or part of the history is hidden by the Bitrix24 account plan; there `meta.total` counts only the ones inside it. Most often it is a request with `filter` or a sort other than `ID`, but a plain request enters the same walk when a page of system notifications leaves the slice short. The **legacy store** returned a full page: `task.commentitem.getlist` is called once and offers no navigation, so a filled page is the only available sign that more may sit behind it; there `meta.total` is what Bitrix24 returned for THIS call, or simply the row count of the page received when it returned no counter of its own; it is not a task-wide counter. A store that was not read at all is NOT reported by this flag — `meta.sources` reports it, by not naming it. The absence of the field only says that the stores that WERE read were read in full — on its own it does not certify the answer complete: an answer is complete when the set is `["chat", "legacy"]` AND the field is absent. ⚠️ On a task with a chat the legacy store is read only when the chat returned NO comments at all, so an answer with `["chat"]` may be missing pre-migration comments that sit in the legacy store |
| `meta.sources` | array | The stores whose contents this response learned: `["chat"]` — the task chat only, `["legacy"]` — the legacy store only, `["chat", "legacy"]` — both, `[]` — neither. A store is listed when it returned comments, or when it was read to the end and proved empty. A store the response learned nothing from is not listed: one that refused, one whose method is missing, or one whose walk broke off before collecting a single comment. ⚠️ An ABSENT store is not that case: when a task has no chat, chat comments cannot exist, the knowledge is complete, and `chat` IS listed. Only a fresh check proves it: within the five-minute cache the same answer honestly says `["legacy"]`, because a chat may have appeared in the meantime. ⚠️ Being listed does NOT mean "read in full": the chat is listed together with `truncated: true` when its walk broke off after collecting at least one comment. `meta.sources` says nothing about completeness — that is what `meta.truncated` reports |

## Response example

```json
{
  "success": true,
  "data": [
    {
      "id": 36559,
      "taskId": 289,
      "authorId": 1,
      "message": "Added a draft of the report to the comments — please take a look.",
      "createdAt": "2026-05-13T12:30:58.000Z",
      "attachments": []
    },
    {
      "id": 36557,
      "taskId": 289,
      "authorId": 79,
      "message": "[USER=99]Alex[/USER], done, thanks.",
      "createdAt": "2026-05-12T15:11:04.000Z",
      "attachments": []
    }
  ],
  "meta": {
    "total": 2,
    "hasMore": false,
    "sources": ["chat"]
  }
}
```

An example of a comment with non-empty `attachments` — [Get comment](./get.md).

## Error response example

400 — `taskId` is not a positive integer:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "taskId must be a positive integer"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_PARAMS` | `taskId` is invalid (not a positive integer) |
| 400 | `INVALID_SORT_FIELD` | Sort by an unsupported field or direction. Supported: `ID`, `AUTHOR_ID`, `AUTHOR_NAME`, `AUTHOR_EMAIL`, `POST_DATE` and their camelCase names, with direction `asc` or `desc`. On the new card `AUTHOR_NAME` and `AUTHOR_EMAIL` respond with `UNSUPPORTED_SORT_FIELD` |
| 400 | `UNSUPPORTED_SORT_FIELD` | New card only: sort by `AUTHOR_NAME` or `AUTHOR_EMAIL`. Sort by `ID`, `AUTHOR_ID` or `POST_DATE` instead |
| 400 | `INVALID_FILTER` | The `filter` parameter is not valid JSON, or it names one field twice in different spellings of the name or of the equality sign, for example `{"AUTHOR_ID":1,"authorId":2}` or `{"AUTHOR_ID":1,"=authorId":2}`. New card only: `filter` is a scalar or an empty array instead of an object (`0`, `false`, `""`, `[]`); the value of `ID` or `AUTHOR_ID` is not a number; the value of `POST_DATE` does not parse as a date; or a field value is an object or an array instead of a scalar. A non-empty array or a non-empty string in `filter` yields `UNKNOWN_FILTER_FIELD` — its indexes are read as field names. An ambiguous envelope is rejected as well: the bracket form and JSON in one request (in either order), or a bracket condition deeper than two levels — half of the conditions are irrecoverably lost during parsing, so the request is refused rather than half-applied. |
| 400 | `UNKNOWN_FILTER_FIELD` | Filter by an unsupported field. Supported: `ID`, `AUTHOR_ID`, `AUTHOR_NAME`, `POST_DATE` and their camelCase names. On the new card `AUTHOR_NAME` responds with `UNSUPPORTED_FILTER_FIELD` |
| 400 | `UNSUPPORTED_FILTER_FIELD` | New card only: filter by `AUTHOR_NAME`. Filter by `ID`, `AUTHOR_ID` or `POST_DATE` instead; look up an employee's identifier by name via `GET /v1/users` |
| 422 | `BITRIX_ERROR` | No task with this `taskId`. The error text comes from Bitrix24 verbatim, with `ERROR_CORE` in the `b24Code` field |
| 403 | `SCOPE_DENIED` | The API key does not have the `task` scope |
| 401 | `TOKEN_MISSING` | The API key has no configured tokens |

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

## Known specifics

**Scan window when filtering.** On the new card a request with `filter` or a sort other than `ID` scans up to 2000 of the newest task chat messages, system notifications included, and picks the matching comments out of them. If the task history is longer, or is limited by the Bitrix24 account plan, the response carries `meta.truncated: true`: comments that are not in the response remained beyond the window edge, and `meta.total` counts only the ones inside it. This is not a sign of a next page — `offset` does not move the window. Narrow the selection with a `POST_DATE` or `AUTHOR_ID` filter. For the same reason `?sort=post_date:asc` on such a task returns the oldest comments within the window, not the oldest comments of the task.

**Filter and sort by author — via `AUTHOR_ID`.** The fields `AUTHOR_NAME` and `AUTHOR_EMAIL` are accepted on the old card and return `400` on the new one, so a request by author name does not work on every Bitrix24 account. To make the selection independent of how the Bitrix24 account is configured, find the employee via `GET /v1/users` and filter or sort by `AUTHOR_ID`.

**The list reads the chat whenever the task has one.** When a task has a chat, the response is assembled from its messages. Comments written before the Bitrix24 account moved to the new card were never migrated into the chat, so when the chat holds no comment of its own the list reads the legacy comment block of the old card and answers from it — `meta.sources` names the stores it actually read. Getting a comment by identifier and downloading an attachment check both stores, but they do NOT guarantee a hit: the lookup goes chat-first, and chat identifiers and old-card identifiers live in separate spaces that overlap numerically, so on a task holding both stores the same number can match a different chat comment and the request never reaches the old card. See "A matching number means nothing" in [Download an attachment](./file-download.md); take a reliable `id` from a response of the same store rather than from somewhere else. If you know the `id`, use [Get comment](./get.md) instead of relying on the list.

**System notifications and the end of the list.** System notifications about task creation, deadline change and assignee change are filtered out on the API side. When a slice consists only of them, the service reads further into the task history instead of returning an empty page that invites more paging: `meta.hasMore: false` with `meta.total: 0` means the source that was read really holds no comments — that is, system notifications do not explain the empty answer. `meta.sources` tells you whether the task itself has none — only `["chat", "legacy"]` is conclusive, see the next item. Such a task used to return `{"data": [], "total": 1, "hasMore": true}` at every `offset`, so a `while (hasMore) offset += limit` loop never ended. Paginate by `meta.hasMore` and the contents of `data`.

**Read an empty `data` through `meta.sources` together with `meta.truncated`.** A conclusive "the task has no comments" comes from the set `["chat", "legacy"]` with no `meta.truncated`: the contents of both stores are known and both are empty. The same set with `truncated: true` means not everything was read — including when the legacy store returned a full page and more comments may sit behind it. Any other set means some store was not read, and an empty `data` says nothing about it. `["legacy"]` — the chat did not answer: it refused (no `im` scope, the method is unavailable), the form of its reply could not be parsed, or the scan broke off. A task that has NO chat yields this set only within the five-minute cache, where the absence of a chat is indistinguishable from an unparsed reply; on a FRESH check its chat store is provably empty and the answer carries the full set. `["chat"]` — the legacy store was not read: the chat returned at least one comment, in which case the follow-up read never starts, or that read failed. `[]` — neither answered. The status stays `200` in every case: the endpoint returns what it learned and names it, instead of failing a request that worked or passing silence off as an answer.

## See also

- [Get comment](./get.md)
- [Create comment](./create.md)
- [Batch comment operations](./comments-batch.md)
- [Tasks](/docs/entities/tasks)
- [Limits and optimization](/docs/optimization)
