API keys
Create a key
Section titled “Create a key”- In the app, open Settings › API. Any workspace member can create their own keys.
- Under New key, pick a name (what it is for), scopes, and an expiration (30 days · 90 days · 365 days · no expiration), then create it.
- The full key (starting with
gtk_) is shown once, right after you create it. Once you close that screen you cannot see it again, so copy it into your secret store right away. The list keeps only the prefix.
One person can hold up to 20 active keys in a workspace. Revoke keys you no longer use on the same screen — a revoked key immediately gets 401 API_KEY_REVOKED.
Scopes
Section titled “Scopes”| Scope | What it allows | Who can pick it |
|---|---|---|
read |
Read issues, comments, workflow states, teams, labels, cycles, wiki spaces and pages, projects, initiatives and milestones; search; list metrics and read metric values | Every member |
write |
Create and update issues, change status, assignee and labels, add comments, create and edit wiki pages, create and edit projects, initiatives and milestones | Owners, admins, members |
metrics:ingest |
Add metric values | Owners, admins, members |
A key acts as the person who created it. Anything that person cannot see now, the key cannot see either, and when their role shrinks the key shrinks with it. If the creator leaves the workspace, the key starts returning 403 at once — create long-running integrations from a shared team account.
Making a request
Section titled “Making a request”curl "https://goaltrack.dyld.kr/api/v1/work_items?limit=50" \ -H "Authorization: Bearer $GOALTRACK_API_KEY"- A key is bound to the workspace it was created in, so you do not need to name the workspace. If you send an
x-workspace-slugheader, it must match the key’s workspace; otherwise you get403 WORKSPACE_MISMATCH. - Lists come back as
{ "items": [...], "next_cursor": "…" }. Fetch the next page withcursor=<next_cursor>; anullnext_cursormeans you are done.limitis 1–200 (default 50). - When you filter the issue list or counts (
GET /api/v1/work_items·/work_items/counts) by team,team_id(orteam_key) returns issues of exactly that team. To include sub-team issues, also sendinclude_sub_teams=true(that team and every team below it).include_sub_teams=truewithoutteam_idorteam_keyreturns400 VALIDATION. - To update an issue, send the
versionyou last read asexpected_versionin thePATCHbody:{ "expected_version": 3, "patch": { "priority": "high" } }. Withoutexpected_versionyou get412 PRECONDITION_REQUIRED; if someone changed it in the meantime you get409 VERSION_CONFLICT. Updating a project, initiative or milestone (PATCH) works the same way. Unknown fields reject the whole request with400 VALIDATION. - A write response to a key without the
readscope holds only what you need to keep writing — issues (create, update, status, assignee){ "id", "short_id", "version" }· pages (create, update){ "id", "slug", "version" }· comments{ "id", "version" }· attaching a label{ "work_item_id", "label_id" }· projects and initiatives{ "id", "version" }· milestones{ "id", "project_id", "version" }. A409 VERSION_CONFLICTcarries onlycurrent_versioninstead of the current content.
Requests open to API keys
Section titled “Requests open to API keys”Only the requests below accept a key. Calling any other request with a key returns 401 UNAUTHORIZED.
| Request | Scope | Per minute |
|---|---|---|
GET /api/v1/work_items |
read |
120 |
GET /api/v1/work_items/:id |
read |
120 |
GET /api/v1/work_items/by-short/:shortId |
read |
120 |
POST /api/v1/work_items |
write |
120 |
PATCH /api/v1/work_items/:id |
write |
120 |
POST /api/v1/work_items/:id/status |
write |
120 |
PATCH /api/v1/work_items/:id/assignee |
write |
120 |
POST /api/v1/work_items/:id/labels |
write |
120 |
DELETE /api/v1/work_items/:id/labels/:labelId |
write |
120 |
GET /api/v1/work_items/:id/comments |
read |
120 |
POST /api/v1/work_items/:id/comments |
write |
120 |
GET /api/v1/workflow_states |
read |
120 |
GET /api/v1/teams |
read |
120 |
GET /api/v1/labels |
read |
120 |
GET /api/v1/cycles |
read |
120 |
GET /api/v1/projects |
read |
120 |
GET /api/v1/projects/:id |
read |
120 |
POST /api/v1/projects |
write |
120 |
PATCH /api/v1/projects/:id |
write |
120 |
GET /api/v1/initiatives |
read |
120 |
GET /api/v1/initiatives/:id |
read |
120 |
POST /api/v1/initiatives |
write |
120 |
PATCH /api/v1/initiatives/:id |
write |
120 |
GET /api/v1/milestones |
read |
120 |
POST /api/v1/milestones |
write |
120 |
PATCH /api/v1/milestones/:id |
write |
120 |
GET /api/v1/wiki_spaces |
read |
120 |
GET /api/v1/documents |
read |
120 |
GET /api/v1/documents/:id |
read |
120 |
GET /api/v1/documents/by-slug/:slug |
read |
120 |
POST /api/v1/documents |
write |
120 |
PATCH /api/v1/documents/:id |
write |
120 |
GET /api/v1/search |
read |
120 |
GET /api/v1/metric_definitions |
read |
120 |
GET /api/v1/metric_definitions/:id/values |
read |
120 |
GET /api/v1/auth/whoami |
read |
120 |
POST <ingest URL> |
metrics:ingest |
120 |
POST <ingest URL>/bulk |
metrics:ingest |
20 |
<ingest URL> is explained in Metric ingest.
Rate limits
Section titled “Rate limits”- Limits count all requests from one key together — any mix of the requests in the table adds up to 120 per minute, reset every minute. Batch ingest is additionally capped at 20 per minute within that. Past the limit you get
429with aRetry-Afterheader (seconds). Wait that long, then retry. - Other abuse-prevention limits also apply (for example, to repeatedly sending a wrong key). Whichever limit you hit, a
429means: wait forRetry-After, then retry.