Skip to content

List comments ​

GET/api/v2/workspaces/{slug}/projects/{project_id}/work-items/{work_item_id}/comments/

Return the comments on one work item. Comments are scoped to their parent work item, so this is the only way to read a work item's discussion. Use it to render a work item detail view or to mirror a thread into another system.

The list is flat. Replies come back as rows of their own, with parent_id set to the top-level comment they answer. Group rows by parent_id to rebuild the threads. Hidden comments are listed too, with hidden_at set.

Path parameters ​

slug:requiredstring

The workspace slug. It appears in your Plane URLs. In https://app.plane.so/my-team/projects/, the slug is my-team.

project_id:requiredstring (uuid)

The project the work item belongs to.

work_item_id:requiredstring (uuid)

The work item whose comments you want. Comments never span work items, so this narrows the result set by itself.

Query parameters ​

Filters

access:optionalstring

Return only comments with this visibility.

  • INTERNAL: visible to the project team
  • EXTERNAL: marked as visible outside the team
external_id:optionalstring

Return comments carrying this identifier from your system. external_id is not a unique key, so this can match more than one comment. Pair it with external_source and handle a multi-row result.

external_source:optionalstring

Return comments that came from this system, for example github or zendesk.

Search

search:optionalstring

Match comments against their plain-text body (comment_stripped), so HTML markup in comment_html never affects whether a term hits.

Ordering

order_by:optionalstring

Field to sort by. Prefix with - for descending. Send it explicitly whenever order matters. The server's default order is not documented, so a thread you render should not depend on it.

  • created_at: oldest first
  • -created_at: newest first
  • id
  • -id

Pagination

per_page:optionalinteger

Page size. Defaults to 50, maximum 200.

offset:optionalinteger

Number of rows to skip from the start of the result set. Maximum 10000. For deeper traversal, switch to cursor pagination.

paginate:optionalstring

Set to cursor to switch to the keyset envelope (no COUNT query) instead of the default offset envelope. The response then carries next_cursor and has_more; send the value of next_cursor back as ?cursor= to fetch the next page. See Pagination for the full envelope.

count:optionalboolean

Defaults to true. Set to false to skip the COUNT(*) and omit total_count from the offset envelope.

access is validated, order_by is not

access is checked against its allowed values. An unrecognized value returns 400 invalid_request instead of a silently empty list. order_by is not checked: an unrecognized value falls back to the default ordering, so a typo shows up as an unexpected sort order instead of an error. Check your spelling there.

Response shaping ​

fields:optionalstring

Comma-separated list of fields to return on each row. Fields you don't request are left out of the response instead of coming back as null, so a missing key means you didn't ask for it and null means the value is null. id is always included, whether or not you name it.

Pass all to get every requestable field. An unknown field name returns 400 with the list of valid names and the closest match, so a typo is caught instead of being silently ignored.

Requestable here: access, actor_id, comment_html, comment_stripped, created_at, created_by_id, edited_at, external_id, external_source, hidden_at, hidden_by_id, hidden_reason, id, parent_id, updated_at, updated_by_id, work_item_id.

See Sparse fields.

expand:optionalstring

Comma-separated relations to embed alongside the ids: actor (the comment author).

Expanded objects get their own key: ?expand=state keeps state_id and adds a state object next to it, so an id is never replaced by an object. An unknown value returns 400.

?fields= and ?expand= take separate sets of names. Relation names aren't valid in ?fields=, field names aren't valid in ?expand=, and field filtering never removes an expanded object: ?fields=id,name&expand=state returns id, name and state. See Expanding relations.

Scopes ​

projects.work_items.comments:read

Errors ​

StatusCodeCause
401unauthorizedMissing or invalid credentials.
402payment_requiredThe feature this endpoint belongs to isn't enabled on your plan, or is switched off.
403forbiddenYour role or token scope can't read this work item.
404not_foundNo such workspace, project, or work item, or it's outside your tenant.
406not_acceptableThe Accept header asks for a representation the API can't produce.
429rate_limitedThrottled. Honor the Retry-After header before retrying.
List comments
bash
curl -X GET \
  "https://api.plane.so/api/v2/workspaces/my-team/projects/4af68566-94a4-4eb3-94aa-50dc9427067b/work-items/8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13/comments/?access=INTERNAL&per_page=50" \
  -H "X-Api-Key: $PLANE_API_KEY"
Response200
json
{
  "data": [
    {
      "id": "c1f7a3d9-2b64-4f80-9c1a-3d5e8b2a6c47",
      "work_item_id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
      "parent_id": null,
      "comment_html": "<p>Deployed the fix to staging. Please re-test.</p>",
      "comment_stripped": "Deployed the fix to staging. Please re-test.",
      "access": "INTERNAL",
      "actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "external_id": null,
      "external_source": null,
      "edited_at": null,
      "hidden_at": null,
      "hidden_by_id": null,
      "hidden_reason": null,
      "created_at": "2026-01-14T09:22:41.478363Z",
      "updated_at": "2026-01-14T09:22:41.478363Z",
      "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "updated_by_id": null
    },
    {
      "id": "a2e04c77-3f19-4d5b-8a6e-91b0c7d2e845",
      "work_item_id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
      "parent_id": null,
      "comment_html": "<p>Re-tested on staging, the login flow is clean now.</p>",
      "comment_stripped": "Re-tested on staging, the login flow is clean now.",
      "access": "INTERNAL",
      "actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "external_id": "ZD-88412",
      "external_source": "zendesk",
      "edited_at": "2026-01-15T11:04:02.115740Z",
      "hidden_at": null,
      "hidden_by_id": null,
      "hidden_reason": null,
      "created_at": "2026-01-15T10:58:19.902314Z",
      "updated_at": "2026-01-15T11:04:02.118326Z",
      "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "updated_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430"
    }
  ],
  "next": null,
  "previous": null,
  "total_count": 2,
  "pagination": {
    "style": "offset"
  }
}
Response200
json
{
  "data": [
    {
      "id": "c1f7a3d9-2b64-4f80-9c1a-3d5e8b2a6c47",
      "work_item_id": "8f4c2b1e-0d3a-4f7b-9c21-6e5a8b7d4f13",
      "parent_id": null,
      "comment_html": "<p>Deployed the fix to staging. Please re-test.</p>",
      "comment_stripped": "Deployed the fix to staging. Please re-test.",
      "access": "INTERNAL",
      "actor_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "external_id": null,
      "external_source": null,
      "edited_at": null,
      "hidden_at": null,
      "hidden_by_id": null,
      "hidden_reason": null,
      "created_at": "2026-01-14T09:22:41.478363Z",
      "updated_at": "2026-01-14T09:22:41.478363Z",
      "created_by_id": "16c61a3a-512a-48ac-b0be-b6b46fe6f430",
      "updated_by_id": null
    }
  ],
  "next_cursor": "b3A9MTcxNDpjMWY3YTNkOQ",
  "has_more": true,
  "pagination": {
    "style": "cursor"
  }
}