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:
identifieris for reading and for the?identifier=filter. The routes/api/v1/issues/{issue}take the numericid. To go fromENG-12to an id, call the list with?identifier=ENG-12.
List issues
/api/v1/workspaces/{workspace}/issuesThe 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 as2026-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:
authentfinds Authentication rewrite,runningdoes not find run. - It composes with the other filters:
?q=login&team=ENGsearches inside that team. - It claims the ordering. With
q, results come back by relevance, not byupdated_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
/api/v1/workspaces/{workspace}/issuesCreates 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
/api/v1/issues/{issue}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
/api/v1/issues/{issue}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
/api/v1/issues/{issue}/archiveThe 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
/api/v1/issues/{issue}Permanently deletes the issue and answers 204. Open boards drop the row live. Prefer archiving.
List comments
/api/v1/issues/{issue}/commentsThe 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
/api/v1/issues/{issue}/commentsPosts 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
/api/v1/comments/{comment}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
/api/v1/issues/{issue}/relationsHow 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
/api/v1/issues/{issue}/relationsLinks 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
/api/v1/issues/{issue}/relationsRemoves 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}'