Issues
Issues live per repo. They share the issues table with pull
requests, but the REST surface here returns issues only (PRs land
in their own section). Markdown bodies are stored raw; the cached
HTML render comes from the same internal/markdown pipeline the
web UI uses.
Issue shape
{
"id": 17,
"number": 1,
"title": "first bug",
"body": "kaboom",
"state": "open",
"state_reason": "",
"locked": false,
"lock_reason": "",
"author_id": 42,
"labels": ["bug"],
"created_at": "2026-05-12T05:00:00Z",
"updated_at": "2026-05-12T05:00:00Z"
}
state is "open" or "closed". state_reason is one of
"completed", "not_planned", "duplicate", "reopened", or
empty when no reason has been recorded. closed_at is present
only on closed issues.
List issues
GET /api/v1/repos/{owner}/{repo}/issues
Required scope: repo:read. Paginated; ?per_page= (≤100) and
?page= apply, with the standard Link: header.
Optional ?state=open|closed|all filter; state=all (or omitted)
returns both.
Pull requests are not included on this endpoint — fetch them via the pulls surface.
Get a single issue
GET /api/v1/repos/{owner}/{repo}/issues/{number}
Required scope: repo:read. 404 when the issue doesn't exist,
when the caller can't see the repo, or when {number} belongs to
a pull request (use the pulls surface).
Create an issue
POST /api/v1/repos/{owner}/{repo}/issues
Required scope: repo:write. Policy: ActionIssueCreate.
{ "title": "first bug", "body": "kaboom" }
| Field | Type | Notes |
|---|---|---|
title |
string | Required, 1–256 chars. |
body |
string | Optional markdown body, ≤65535 chars. |
Returns 201 with the issue envelope.
Errors
| Status | When |
|---|---|
| 401 | PAT missing/invalid. |
| 403 | PAT lacks repo:write scope or policy denial. |
| 422 | Empty title, title too long, body too long. |
Update an issue
PATCH /api/v1/repos/{owner}/{repo}/issues/{number}
Required scope: repo:write. Only the fields you send are
modified; everything else stays.
{
"title": "first bug — root cause found",
"body": "see comment #3",
"state": "closed",
"state_reason": "completed"
}
Permission rules:
- Title / body — the issue author OR a repo collaborator with
write access. Other callers
403. - State / state_reason — any caller with
ActionIssueCloseon the repo.state_reasonmust be one ofcompleted,not_planned,duplicate,reopened(or empty).
Returns the freshly-loaded issue.
Lock and unlock
PUT /api/v1/repos/{owner}/{repo}/issues/{number}/lock
DELETE /api/v1/repos/{owner}/{repo}/issues/{number}/lock
Required scope: repo:write. PUT body is optional:
{ "lock_reason": "off-topic" }
Returns 204. Locking refuses non-collaborator comments; the
issue itself stays visible.
Comments
List
GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments
Required scope: repo:read. Returns comments in chronological
order.
Add
POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments
Required scope: repo:write. Policy: ActionIssueComment. Body:
{ "body": "lgtm" }
Subject to the per-author comment rate limit (20/hour); 429
when exceeded.
Edit own comment
PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{cid}
Required scope: repo:write. The comment author can edit their
own; other callers 403.
Delete a comment
DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{cid}
Required scope: repo:write. The comment author can delete their
own; repo collaborators with write access can delete any comment
on the repo (moderation affordance, matches the gh shape).
Returns 204.
Not yet shipped
POST /api/v1/repos/{o}/{r}/issues/{n}/transfer, pinning,
applying labels / assignees / milestones over the issue PATCH —
all queued for a follow-up batch.
View source
| 1 | # Issues |
| 2 | |
| 3 | Issues live per repo. They share the `issues` table with pull |
| 4 | requests, but the REST surface here returns issues only (PRs land |
| 5 | in their own section). Markdown bodies are stored raw; the cached |
| 6 | HTML render comes from the same `internal/markdown` pipeline the |
| 7 | web UI uses. |
| 8 | |
| 9 | ## Issue shape |
| 10 | |
| 11 | ```json |
| 12 | { |
| 13 | "id": 17, |
| 14 | "number": 1, |
| 15 | "title": "first bug", |
| 16 | "body": "kaboom", |
| 17 | "state": "open", |
| 18 | "state_reason": "", |
| 19 | "locked": false, |
| 20 | "lock_reason": "", |
| 21 | "author_id": 42, |
| 22 | "labels": ["bug"], |
| 23 | "created_at": "2026-05-12T05:00:00Z", |
| 24 | "updated_at": "2026-05-12T05:00:00Z" |
| 25 | } |
| 26 | ``` |
| 27 | |
| 28 | `state` is `"open"` or `"closed"`. `state_reason` is one of |
| 29 | `"completed"`, `"not_planned"`, `"duplicate"`, `"reopened"`, or |
| 30 | empty when no reason has been recorded. `closed_at` is present |
| 31 | only on closed issues. |
| 32 | |
| 33 | ## List issues |
| 34 | |
| 35 | ``` |
| 36 | GET /api/v1/repos/{owner}/{repo}/issues |
| 37 | ``` |
| 38 | |
| 39 | Required scope: `repo:read`. Paginated; `?per_page=` (≤100) and |
| 40 | `?page=` apply, with the standard `Link:` header. |
| 41 | |
| 42 | Optional `?state=open|closed|all` filter; `state=all` (or omitted) |
| 43 | returns both. |
| 44 | |
| 45 | Pull requests are not included on this endpoint — fetch them via |
| 46 | the pulls surface. |
| 47 | |
| 48 | ## Get a single issue |
| 49 | |
| 50 | ``` |
| 51 | GET /api/v1/repos/{owner}/{repo}/issues/{number} |
| 52 | ``` |
| 53 | |
| 54 | Required scope: `repo:read`. `404` when the issue doesn't exist, |
| 55 | when the caller can't see the repo, or when `{number}` belongs to |
| 56 | a pull request (use the pulls surface). |
| 57 | |
| 58 | ## Create an issue |
| 59 | |
| 60 | ``` |
| 61 | POST /api/v1/repos/{owner}/{repo}/issues |
| 62 | ``` |
| 63 | |
| 64 | Required scope: `repo:write`. Policy: `ActionIssueCreate`. |
| 65 | |
| 66 | ```json |
| 67 | { "title": "first bug", "body": "kaboom" } |
| 68 | ``` |
| 69 | |
| 70 | | Field | Type | Notes | |
| 71 | |---------|--------|-----------------------------------------------| |
| 72 | | `title` | string | Required, 1–256 chars. | |
| 73 | | `body` | string | Optional markdown body, ≤65535 chars. | |
| 74 | |
| 75 | Returns `201` with the issue envelope. |
| 76 | |
| 77 | ### Errors |
| 78 | |
| 79 | | Status | When | |
| 80 | |-------:|---------------------------------------------------| |
| 81 | | 401 | PAT missing/invalid. | |
| 82 | | 403 | PAT lacks `repo:write` scope or policy denial. | |
| 83 | | 422 | Empty title, title too long, body too long. | |
| 84 | |
| 85 | ## Update an issue |
| 86 | |
| 87 | ``` |
| 88 | PATCH /api/v1/repos/{owner}/{repo}/issues/{number} |
| 89 | ``` |
| 90 | |
| 91 | Required scope: `repo:write`. Only the fields you send are |
| 92 | modified; everything else stays. |
| 93 | |
| 94 | ```json |
| 95 | { |
| 96 | "title": "first bug — root cause found", |
| 97 | "body": "see comment #3", |
| 98 | "state": "closed", |
| 99 | "state_reason": "completed" |
| 100 | } |
| 101 | ``` |
| 102 | |
| 103 | Permission rules: |
| 104 | |
| 105 | - **Title / body** — the issue author OR a repo collaborator with |
| 106 | write access. Other callers `403`. |
| 107 | - **State / state_reason** — any caller with `ActionIssueClose` |
| 108 | on the repo. `state_reason` must be one of `completed`, |
| 109 | `not_planned`, `duplicate`, `reopened` (or empty). |
| 110 | |
| 111 | Returns the freshly-loaded issue. |
| 112 | |
| 113 | ## Lock and unlock |
| 114 | |
| 115 | ``` |
| 116 | PUT /api/v1/repos/{owner}/{repo}/issues/{number}/lock |
| 117 | DELETE /api/v1/repos/{owner}/{repo}/issues/{number}/lock |
| 118 | ``` |
| 119 | |
| 120 | Required scope: `repo:write`. PUT body is optional: |
| 121 | |
| 122 | ```json |
| 123 | { "lock_reason": "off-topic" } |
| 124 | ``` |
| 125 | |
| 126 | Returns `204`. Locking refuses non-collaborator comments; the |
| 127 | issue itself stays visible. |
| 128 | |
| 129 | ## Comments |
| 130 | |
| 131 | ### List |
| 132 | |
| 133 | ``` |
| 134 | GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments |
| 135 | ``` |
| 136 | |
| 137 | Required scope: `repo:read`. Returns comments in chronological |
| 138 | order. |
| 139 | |
| 140 | ### Add |
| 141 | |
| 142 | ``` |
| 143 | POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments |
| 144 | ``` |
| 145 | |
| 146 | Required scope: `repo:write`. Policy: `ActionIssueComment`. Body: |
| 147 | |
| 148 | ```json |
| 149 | { "body": "lgtm" } |
| 150 | ``` |
| 151 | |
| 152 | Subject to the per-author comment rate limit (20/hour); `429` |
| 153 | when exceeded. |
| 154 | |
| 155 | ### Edit own comment |
| 156 | |
| 157 | ``` |
| 158 | PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{cid} |
| 159 | ``` |
| 160 | |
| 161 | Required scope: `repo:write`. The comment author can edit their |
| 162 | own; other callers `403`. |
| 163 | |
| 164 | ### Delete a comment |
| 165 | |
| 166 | ``` |
| 167 | DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{cid} |
| 168 | ``` |
| 169 | |
| 170 | Required scope: `repo:write`. The comment author can delete their |
| 171 | own; repo collaborators with write access can delete any comment |
| 172 | on the repo (moderation affordance, matches the gh shape). |
| 173 | |
| 174 | Returns `204`. |
| 175 | |
| 176 | ## Not yet shipped |
| 177 | |
| 178 | `POST /api/v1/repos/{o}/{r}/issues/{n}/transfer`, pinning, |
| 179 | applying labels / assignees / milestones over the issue PATCH — |
| 180 | all queued for a follow-up batch. |