PolderAPI

Backlog API

Issues

List, search, create, change, archive and delete issues, and work with their comments and relations.

Base URLhttps://backlog.polder.work

Issues are the core of the Backlog API. Reading needs issues:read, everything that changes something needs issues:write. Every request also runs the same policies as the web app: a private team you are not in is invisible (404 in lists, 403 on a direct id), and writing needs permission to contribute to the issue's team.

Writes go through the same actions as the web app, so numbers are claimed atomically, descriptions and comments are sanitised, notifications and webhooks fire, and boards update live.

The issue object

{
  "id": 981,
  "identifier": "ENG-12",
  "number": 12,
  "team_id": 3,
  "title": "Login times out",
  "description": "<p>…</p>",
  "state": { "id": 21, "team_id": 3, "name": "In Progress", "type": "started", "color": "#f59e0b", "progress": 0.5, "position": 2 },
  "priority": 2,
  "priority_name": "High",
  "estimate": 3,
  "budget_minutes": null,
  "assignees": [{ "id": 1, "name": "Sanne", "email": "sanne@example.com", "isBot": false }],
  "reviewers": [],
  "creator": { "id": 2, "name": "Daan", "email": "daan@example.com", "isBot": false },
  "project_id": null,
  "milestone_id": null,
  "cycle_id": null,
  "parent_id": null,
  "labels": [{ "id": 12, "name": "bug", "color": "#ef4444", "team_id": null, "group_id": null, "is_group": false }],
  "customers": [],
  "due_at": "2026-09-01",
  "scheduled_days": ["2026-08-28"],
  "scheduled_times": {},
  "scheduled_for": "2026-08-28",
  "started_at": "2026-08-20T09:00:00+00:00",
  "completed_at": null,
  "canceled_at": null,
  "state_changed_at": "2026-08-20T09:00:00+00:00",
  "archived_at": null,
  "sla": { "targets": null, "due_at": null, "at_risk_at": null, "breached_at": null },
  "is_favorite": false,
  "triage_status": null,
  "created_at": "2026-08-19T14:00:00+00:00",
  "updated_at": "2026-08-20T09:00:00+00:00"
}
Field Description
id Numeric id. Used in /api/v1/issues/{issue} and everywhere a field takes an issue.
identifier TEAMKEY-number, such as ENG-12. This is what people paste into a chat.
number The number inside the team. Unique per team, not per workspace.
priority 0 none, 1 urgent, 2 high, 3 medium, 4 low. priority_name is the English name of it (None, Urgent, High, Medium, Low).
state The workflow state, as in Workspace. Switch on state.type, not state.name.
estimate, budget_minutes Integers or null.
assignees, reviewers People, as in members.
creator Who filed it, or null if that person is gone. There is exactly one.
due_at A date (YYYY-MM-DD) or null.
scheduled_days, scheduled_times, scheduled_for The days planned for the issue. scheduled_for is the first of scheduled_days, kept for older integrations.
started_at … archived_at ISO 8601 timestamps, null until they happen.
sla The SLA timestamps; targets is the number of SLA targets.
is_favorite Whether the token's owner starred it. Absent, not false, where nobody asked (the archive response).
triage_status pending, accepted, declined, merged, or null for an issue that never went through triage.
customers Customers linked to the issue. Their contact details are absent for guests.

customers is only included by the single-issue, create and update responses; identifier, state and the other relation fields are only present where the relation was loaded, which is always the case for the endpoints below.

Note: identifier is for reading and for the ?identifier= filter. The routes /api/v1/issues/{issue} take the numeric id. To go from ENG-12 to an id, call the list with ?identifier=ENG-12.

List issues

GET/api/v1/workspaces/{workspace}/issuesscope issues:read

The issues in the workspace you may see, archived ones excluded. Every filter is optional and only narrows the set.

Query parameter Description
team A team key, such as ENG (case-insensitive, up to 10 characters).
project_id Issues in that project.
cycle_id Issues in that cycle.
assignee_id Issues that user is one of the assignees of.
creator_id Issues that user filed. Not the same question as assignee_id.
state_type backlog, unstarted, started, completed or canceled.
identifier One identifier, such as ENG-123. Matches nothing, not everything, when it is a bare number (123), an unknown key, or not an identifier at all.
favorite 1 / true to only return what the token's owner starred.
updated_since Only issues updated at or after that moment. Must be a date.
q Free-text search, up to 255 characters. See below.
page, per_page Default 50, maximum 200.

A filter value of the wrong type (for example state_type=done) is a 422.

Without q the list is ordered by updated_at (oldest first), then id, which makes updated_since polling stable: ask for what moved since your last run and page through that window without rows shifting.

Warning: URL-encode updated_since. An ISO 8601 stamp ends in +00:00, and a bare + in a query string decodes to a space, so the value arrives as 2026-08-18T12:00:00 00:00.

Searching

q is the same search the command palette uses: titles, descriptions and comments, ranked, with an exact identifier (ENG-12) first.

  • It is prefix-based. Every typed word is matched as a prefix, and nothing is stemmed: authent finds Authentication rewrite, running does not find run.
  • It composes with the other filters: ?q=login&team=ENG searches inside that team.
  • It claims the ordering. With q, results come back by relevance, not by updated_at.
  • A term with nothing searchable in it (?q=...) matches nothing.
curl "https://backlog.polder.work/api/v1/workspaces/7/issues?team=ENG&state_type=started&per_page=20" \
  -H "Authorization: Bearer $POLDER_TOKEN"
{
  "data": [ { "id": 981, "identifier": "ENG-12", "title": "Login times out", … } ],
  "meta": { "page": 1, "per_page": 20, "total": 1, "last_page": 1 }
}

The list items omit customers. Each one carries is_favorite.

Create an issue

POST/api/v1/workspaces/{workspace}/issuesscope issues:write

Creates an issue and answers 201 with the issue object. The team is named in the body, by key, because it is a choice the caller makes per request. Creating needs permission to contribute to that team (403).

Side effects: the next number in the team is claimed, the creator is you, the assignees and reviewers are notified, mentions in the description notify the people mentioned, and the team's automations and workflows run. If the team has a default reviewer and you do not send reviewer_ids, that reviewer is added.

Body

Field Type Required Description
team string yes Team key, such as ENG. An unknown key is a 422 ("Unknown team.").
title string yes Up to 255 characters.
description string no HTML. It is sanitised; markup the sanitiser rejects is dropped.
workflow_state_id integer no A state of that team. Defaults to the team's default state.
priority integer no 0–4, see the object. Defaults to 0.
estimate integer no 0–1000.
budget_minutes integer no 0 up to the maximum the workspace allows.
assignee_ids integer[] no User ids, from members.
reviewer_ids integer[] no User ids.
project_id integer no A project.
milestone_id integer no A milestone of project_id.
cycle_id integer no A cycle of the team.
parent_id integer no An issue of the same team, to make this a sub-issue.
due_at date no YYYY-MM-DD.
label_ids integer[] no Label ids.
customer_ids integer[] no Customer ids.
position string no top (default) or bottom: where it lands in its column.

Ids that do not exist, or belong to another team, are a 422. Ids of a project, label, assignee or reviewer that exist but do not belong in this team or workspace are silently left out rather than refused. scheduled_days and scheduled_for are not accepted on create; set them with a PATCH.

curl https://backlog.polder.work/api/v1/workspaces/7/issues \
  -H "Authorization: Bearer $POLDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"team": "ENG", "title": "Login times out", "priority": 2, "assignee_ids": [1]}'
{ "data": { "id": 981, "identifier": "ENG-12", "title": "Login times out", … } }

The MCP equivalent is polder_create_issue.

Read an issue

GET/api/v1/issues/{issue}scope issues:read

One issue by numeric id, including customers and is_favorite. 403 if you may not see its team.

curl https://backlog.polder.work/api/v1/issues/981 \
  -H "Authorization: Bearer $POLDER_TOKEN"

Change an issue

PATCH/api/v1/issues/{issue}scope issues:write

Changes only the fields you send. Omitted fields are left alone, which is what stops two integrations from overwriting each other. Send null to clear a nullable field. Answers 200 with the new issue.

To move an issue, send a workflow_state_id from the issue's team. Timestamps such as started_at and completed_at follow the state's type.

Body (all optional)

Field Type Description
title string Up to 255 characters, not null.
description string or null HTML, sanitised.
workflow_state_id integer A state of the issue's team.
priority integer 0–4.
estimate, budget_minutes integer or null As on create.
assignee_ids, reviewer_ids, label_ids, customer_ids integer[] Replace the whole set. [] removes everyone.
project_id, milestone_id integer or null Existing project / milestone.
cycle_id integer or null A cycle of the team.
parent_id integer or null An issue of the same team.
due_at date or null YYYY-MM-DD.
scheduled_days date[] or null The days planned for it. Replaces the plan; [] or null clears it.
scheduled_for date or null The single-day form of scheduled_days. If you send both, scheduled_days wins.

position is not accepted on update.

curl -X PATCH https://backlog.polder.work/api/v1/issues/981 \
  -H "Authorization: Bearer $POLDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"workflow_state_id": 22, "assignee_ids": [1, 4]}'

Failing validation is a 422 with the field names under errors.

Archive an issue

POST/api/v1/issues/{issue}/archivescope issues:write

The reversible way to get rid of an issue. No body. Answers 200 with the issue, with archived_at set. Archived issues no longer appear in the list. It needs the same permission as a PATCH. A PATCH cannot archive: use this.

Delete an issue

DELETE/api/v1/issues/{issue}scope issues:write

Permanently deletes the issue and answers 204. Open boards drop the row live. Prefer archiving.

List comments

GET/api/v1/issues/{issue}/commentsscope issues:read

The comments on an issue, oldest first, paged.

{
  "data": [
    {
      "id": 77, "issue_id": 981, "parent_id": null, "body": "<p>On it.</p>",
      "author": { "id": 1, "name": "Sanne", "email": "sanne@example.com", "isBot": false },
      "edited_at": null,
      "created_at": "2026-08-20T09:05:00+00:00"
    }
  ],
  "meta": { … }
}

Comments are flat in this API: a reply carries the parent_id of the thread's first comment.

Post a comment

POST/api/v1/issues/{issue}/commentsscope issues:write

Posts a comment as the token's owner and answers 201 with it. Needs permission to contribute to the issue's team. Side effects: the issue's subscribers are notified, people mentioned are notified, and on an issue that came in by email a public comment may be sent back to the sender.

Body

Field Type Required Description
body string yes HTML. Sanitised: a body with nothing left after sanitising is a 422.
parent_id integer no A comment on the same issue. A reply to a reply is attached to the thread's first comment.
curl https://backlog.polder.work/api/v1/issues/981/comments \
  -H "Authorization: Bearer $POLDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body": "<p>Fixed in the next release.</p>"}'

Comments posted through the API are internal, never public.

Delete a comment

DELETE/api/v1/comments/{comment}scope issues:write

Deletes a comment and answers 204. Only its author may do this (403 for anyone else), as in the web app. There is no endpoint to edit a comment.

List relations

GET/api/v1/issues/{issue}/relationsscope issues:read

How the issue hangs together with other issues, from this issue's point of view. Not paged, and the envelope has no meta.

{
  "data": [
    { "type": "blocks", "issue_id": 982, "identifier": "ENG-13", "title": "Ship the release" },
    { "type": "blocked_by", "issue_id": 970, "identifier": "ENG-4", "title": "Rotate the keys" }
  ]
}
type Meaning, from this issue's point of view
blocks This issue blocks the other one.
blocked_by The other issue blocks this one.
related Related, in both directions.
duplicate This issue is the original of which the other is a duplicate.
duplicate_of This issue is a duplicate of the other.

The list includes relations you have no right to see the other side of: the identifier and title come back for every related issue.

Add a relation

POST/api/v1/issues/{issue}/relationsscope issues:write

Links two issues and answers 204. Needs permission to change {issue} and to see the other issue.

Body

Field Type Required Description
type string yes blocks, blocked_by, related, duplicate or duplicate_of.
issue_id integer yes The numeric id of the other issue.

blocked_by and duplicate_of are folded into blocks and duplicate with the two sides swapped, so either direction produces the same graph. Adding a relation that already exists is a no-op. A new relation is recorded in the activity of both issues.

curl https://backlog.polder.work/api/v1/issues/981/relations \
  -H "Authorization: Bearer $POLDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "blocked_by", "issue_id": 970}'

Remove a relation

DELETE/api/v1/issues/{issue}/relationsscope issues:write

Removes a relation and answers 204. It takes the same body as adding one, in the body of the DELETE, not in the URL: type and issue_id. Removing a relation that is not there is not an error.

curl -X DELETE https://backlog.polder.work/api/v1/issues/981/relations \
  -H "Authorization: Bearer $POLDER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type": "blocked_by", "issue_id": 970}'