markdown · 6215 bytes Raw Blame History

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",
  "labels":    ["bug", "needs-triage"],
  "assignees": ["alice"],
  "milestone": 7
}

Permission rules:

  • Title / body — the issue author OR a repo collaborator with write access. Other callers 403.
  • State / state_reason — any caller with ActionIssueClose on the repo. state_reason must be one of completed, not_planned, duplicate, reopened (or empty).
  • Labels — caller needs ActionIssueLabel. The payload is a full replace: ["bug"] strips every other label; [] clears them all. Omit the field to leave labels untouched. Unknown label names return 422.
  • Assignees — caller needs ActionIssueAssign. Same full-replace semantics, names are usernames; unknown usernames return 422.
  • Milestone — caller needs ActionIssueAssign. Pass the milestone id (see Milestones below); 0 clears the milestone. The milestone must belong to the same repo; cross-repo ids return 422.

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.

Milestones

GET    /api/v1/repos/{owner}/{repo}/milestones[?state=open|closed|all]
POST   /api/v1/repos/{owner}/{repo}/milestones
GET    /api/v1/repos/{owner}/{repo}/milestones/{id}
PATCH  /api/v1/repos/{owner}/{repo}/milestones/{id}
DELETE /api/v1/repos/{owner}/{repo}/milestones/{id}

Required scope: repo:read on GETs, repo:write on mutations. Mutations gate on ActionIssueLabel (write collaborator role on the repo).

Create payload:

{
  "title":       "v1.0",
  "description": "first stable release",
  "due_on":      "2026-06-01T00:00:00Z",
  "state":       "open"
}

due_on is RFC3339; omit or pass null to leave it unset. The response shape mirrors GitHub's milestone with the repo-local id, the state (open / closed), and live open_issues / closed_issues counters.

Identifier: the path takes the milestone primary key id (returned by list / create), not a per-repo number — shithub's schema doesn't carry a number column. The CLI gets the id back from POST or list responses.

Assignees

GET /api/v1/repos/{owner}/{repo}/assignees

Required scope: repo:read. Returns the users eligible to be assigned: the repo owner (when user-owned) plus every direct repo collaborator. Org-level expansion of org-owned repos is a follow-up.

Not yet shipped

POST /api/v1/repos/{o}/{r}/issues/{n}/transfer and issue pinning are 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 "labels": ["bug", "needs-triage"],
101 "assignees": ["alice"],
102 "milestone": 7
103 }
104 ```
105
106 Permission rules:
107
108 - **Title / body** — the issue author OR a repo collaborator with
109 write access. Other callers `403`.
110 - **State / state_reason** — any caller with `ActionIssueClose`
111 on the repo. `state_reason` must be one of `completed`,
112 `not_planned`, `duplicate`, `reopened` (or empty).
113 - **Labels** — caller needs `ActionIssueLabel`. The payload is a
114 *full replace*: `["bug"]` strips every other label;
115 `[]` clears them all. Omit the field to leave labels untouched.
116 Unknown label names return `422`.
117 - **Assignees** — caller needs `ActionIssueAssign`. Same
118 full-replace semantics, names are usernames; unknown usernames
119 return `422`.
120 - **Milestone** — caller needs `ActionIssueAssign`. Pass the
121 milestone `id` (see [Milestones](#milestones) below); `0`
122 clears the milestone. The milestone must belong to the same
123 repo; cross-repo ids return `422`.
124
125 Returns the freshly-loaded issue.
126
127 ## Lock and unlock
128
129 ```
130 PUT /api/v1/repos/{owner}/{repo}/issues/{number}/lock
131 DELETE /api/v1/repos/{owner}/{repo}/issues/{number}/lock
132 ```
133
134 Required scope: `repo:write`. PUT body is optional:
135
136 ```json
137 { "lock_reason": "off-topic" }
138 ```
139
140 Returns `204`. Locking refuses non-collaborator comments; the
141 issue itself stays visible.
142
143 ## Comments
144
145 ### List
146
147 ```
148 GET /api/v1/repos/{owner}/{repo}/issues/{number}/comments
149 ```
150
151 Required scope: `repo:read`. Returns comments in chronological
152 order.
153
154 ### Add
155
156 ```
157 POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments
158 ```
159
160 Required scope: `repo:write`. Policy: `ActionIssueComment`. Body:
161
162 ```json
163 { "body": "lgtm" }
164 ```
165
166 Subject to the per-author comment rate limit (20/hour); `429`
167 when exceeded.
168
169 ### Edit own comment
170
171 ```
172 PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{cid}
173 ```
174
175 Required scope: `repo:write`. The comment author can edit their
176 own; other callers `403`.
177
178 ### Delete a comment
179
180 ```
181 DELETE /api/v1/repos/{owner}/{repo}/issues/comments/{cid}
182 ```
183
184 Required scope: `repo:write`. The comment author can delete their
185 own; repo collaborators with write access can delete any comment
186 on the repo (moderation affordance, matches the gh shape).
187
188 Returns `204`.
189
190 ## Milestones
191
192 ```
193 GET /api/v1/repos/{owner}/{repo}/milestones[?state=open|closed|all]
194 POST /api/v1/repos/{owner}/{repo}/milestones
195 GET /api/v1/repos/{owner}/{repo}/milestones/{id}
196 PATCH /api/v1/repos/{owner}/{repo}/milestones/{id}
197 DELETE /api/v1/repos/{owner}/{repo}/milestones/{id}
198 ```
199
200 Required scope: `repo:read` on GETs, `repo:write` on mutations.
201 Mutations gate on `ActionIssueLabel` (write collaborator role
202 on the repo).
203
204 Create payload:
205
206 ```json
207 {
208 "title": "v1.0",
209 "description": "first stable release",
210 "due_on": "2026-06-01T00:00:00Z",
211 "state": "open"
212 }
213 ```
214
215 `due_on` is RFC3339; omit or pass `null` to leave it unset. The
216 response shape mirrors GitHub's milestone with the repo-local
217 `id`, the `state` (`open` / `closed`), and live `open_issues` /
218 `closed_issues` counters.
219
220 Identifier: the path takes the milestone primary key `id`
221 (returned by list / create), not a per-repo number — shithub's
222 schema doesn't carry a number column. The CLI gets the id back
223 from `POST` or list responses.
224
225 ## Assignees
226
227 ```
228 GET /api/v1/repos/{owner}/{repo}/assignees
229 ```
230
231 Required scope: `repo:read`. Returns the users eligible to be
232 assigned: the repo owner (when user-owned) plus every direct
233 repo collaborator. Org-level expansion of org-owned repos is a
234 follow-up.
235
236 ## Not yet shipped
237
238 `POST /api/v1/repos/{o}/{r}/issues/{n}/transfer` and issue
239 pinning are queued for a follow-up batch.