REST API
Read and write Skopo projects, work items, comments, sprints, and planning records over HTTPS with a personal access token.
Skopo has a JSON REST API at https://skopo.dev/api/v1. Use it from scripts, CI jobs, or internal tools when an MCP client isn't the right fit. It uses the same tokens, scopes, and permissions as MCP access: you only see projects your account can see, and observers can comment but can't edit work.
Authentication
Create a personal token in Settings → MCP access → Personal tokens and send it as a bearer token:
curl https://skopo.dev/api/v1/projects \
-H "Authorization: Bearer $SKOPO_TOKEN"
| Scope | Needed for |
|---|---|
work_items:read | Every GET endpoint |
work_items:write | Creating and updating work items and planning records |
comments:write | Adding comments (work_items:write also works) |
Keep tokens out of source control. Revoke a token from the same settings list when you stop using it.
Errors
Errors return a JSON body with a message, a machine-readable code, and a request id:
{ "error": "Project not found", "code": "NOT_FOUND", "requestId": "req_..." }
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Invalid field, unknown workflow state, or an assignee who isn't on the project |
| 401 | UNAUTHORIZED | Missing, invalid, expired, or revoked token |
| 403 | FORBIDDEN | Token lacks the scope, or your project role can't make the change |
| 404 | NOT_FOUND | Project or work item doesn't exist or isn't visible to you |
| 409 | CONFLICT | A methodology transition gate blocked the move (WIP limit, required policy, and so on; details.violations explains why), or a release failed its readiness gates (details.readiness). A sprint change that doesn't fit the sprint's state also returns 409. |
Projects
GET /api/v1/projects
Lists your active projects. Each project includes its id, publicId (prj_...), key, name, the active methodology, workflow states, and work-item types. Every {projectId} below accepts either the publicId or the UUID.
GET /api/v1/projects/{projectId}/members
Lists who can be assigned in the project. people have id, name, email, role (project role), and availability; pass a person's id as assigneeId on work items. agents have id, name, role, templateId, status, and availability. Requires work_items:read.
GET /api/v1/projects/{projectId}/sprints
Lists sprints with id, name, goal, startDate, endDate, capacity, state (planned, active, completed, or unscheduled), startedAt, and completedAt.
The sprint endpoints below need the work_items:write scope, a project whose methodology uses sprints (Scrum, or Scrumban with the Sprint goals practice), and the Lead project role or workspace Owner role. Otherwise they return 400 for a methodology without sprints and 403 for other roles. A sprint id that isn't in the project returns 404.
POST /api/v1/projects/{projectId}/sprints
{ "name": "Sprint 3", "goal": "Ship checkout", "startDate": "2026-11-02", "endDate": "2026-11-13", "capacity": 20 }
name is required; goal, startDate, endDate (YYYY-MM-DD), and capacity (points) are optional. Returns 201 with the new sprint.
PATCH /api/v1/projects/{projectId}/sprints/{sprintId}
Send only the fields you want to change, from the same set as create. Send null to clear goal, a date, or capacity. A completed sprint can't be edited (409).
POST /api/v1/projects/{projectId}/sprints/{sprintId}/start
Starts the sprint today. The planned end date is kept; if there is none, it follows the project's sprint length. An optional body { "endDate": "YYYY-MM-DD" } overrides it. Returns 409 if the sprint is already started or completed, or another sprint is still active.
POST /api/v1/projects/{projectId}/sprints/{sprintId}/complete
{ "rolloverSprintId": "7d1c…" }
Completes the active sprint. rolloverSprintId is required: unfinished items move to that sprint, or back to the backlog when it is null. Done items stay with the completed sprint. The response has the completed sprint, completedItems and rolledOverItems (work item keys), and rolledOverTo. Returns 409 if the sprint isn't active.
Work items
GET /api/v1/projects/{projectId}/work-items
| Query parameter | Description |
|---|---|
status | A workflow state from the project's methodology, by id (inprogress) or label (In Progress, Building), case-insensitive |
workItemTypeKey | A work item type from the project's methodology, by key or label |
assignee | A user id, me, or none |
sprintId | A sprint id, or none for items outside any sprint |
priority | urgent, high, medium, low, or none |
blocked | true or false |
q | Text matched against the title and key |
limit | 1–200, default 50 |
cursor | The nextCursor value from the previous page |
compact | true returns only key, displayRef, title, status, statusLabel, workItemTypeKey, priority, points, assigneeId, blocked, and updatedAt for each item |
fields | Comma-separated item fields to return instead, such as title,status. key is always included. |
Filters run in the database, so paging stays fast on large projects. The response has items, total (all matching items), and nextCursor (null on the last page). Each item has key, status (the workflow state id), and statusLabel (its label in the project's methodology). An unknown status or type returns 400 with the project's valid values.
POST /api/v1/projects/{projectId}/work-items
curl -X POST https://skopo.dev/api/v1/projects/prj_S8qDRLbDJWbp/work-items \
-H "Authorization: Bearer $SKOPO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Fix login redirect","status":"todo","priority":"high","dueDate":"2026-11-01"}'
title is required. Optional fields: description, status, workItemTypeKey, points, assigneeId, sprintId, parentIssueId, priority, dueDate and startDate (YYYY-MM-DD), and labels ([{ "t": "name", "c": "#2A6FDB" }]). The status and type must exist in the project's methodology; pass either the id or the label, case-insensitive. Unknown values return 400 listing the valid ones. Returns 201 with the created item, its key, status, and statusLabel.
GET /api/v1/work-items/{key}?projectId={projectId}
Returns full detail for one work item, including activity and comments. Images embedded in the description or comments use absolute URLs and are listed in item.attachments (id, filename, mimeType, byteSize, url). Fetch an image by sending a GET to its url with the same Authorization: Bearer header; it needs the work_items:read scope.
PATCH /api/v1/work-items/{key}?projectId={projectId}
Send only the fields you want to change. The same fields as create are accepted except workItemTypeKey. Send null to clear assigneeId, sprintId, points, priority, or a date. status accepts a workflow state id or label. Status moves go through the methodology's transition gates, just like moving a card on the board.
POST /api/v1/work-items/{key}/comments?projectId={projectId}
{ "content": "Deployed to staging." }
Adds a comment and sends the usual notifications. Returns 201.
Planning records
Goals, risks, decisions, dependencies, and releases follow the same rules as the Delivery and Operations views. Every GET needs work_items:read; every POST and PATCH needs work_items:write and a project role that can edit work. POST returns 201. PATCH changes only the fields you send and returns 404 when the record isn't in the project. Dates use YYYY-MM-DD.
| Endpoint | Body fields |
|---|---|
GET /api/v1/projects/{projectId}/goals | Returns goals with owner, targetDate, measure, and target and current values. |
POST /api/v1/projects/{projectId}/goals | title (required), description, targetDate, measure, targetValue. New goals start active. |
PATCH /api/v1/projects/{projectId}/goals/{goalId} | title, description, status (draft, active, at_risk, achieved), measure, targetValue, currentValue, targetDate (null clears it). |
GET /api/v1/projects/{projectId}/risks | Optional ?status= (open, monitoring, mitigated, closed). |
POST /api/v1/projects/{projectId}/risks | title (required), description, probability and impact (low, medium, high), mitigation, ownerId, reviewDate. |
PATCH /api/v1/projects/{projectId}/risks/{riskId} | status, probability, impact, mitigation. |
GET /api/v1/projects/{projectId}/decisions | Optional ?status= (proposed, decided, superseded). |
POST /api/v1/projects/{projectId}/decisions | title (required), context, options, decision, consequences, ownerId. New decisions start proposed. |
PATCH /api/v1/projects/{projectId}/decisions/{decisionId} | status, decision, consequences. Setting decided records decidedAt. |
GET /api/v1/projects/{projectId}/dependencies | Optional ?status= (open, at_risk, resolved). Each dependency includes sourceWorkItem, targetWorkItem, and targetProject. |
POST /api/v1/projects/{projectId}/dependencies | title (required), and at least one of sourceWorkItemKey (a work item in this project), targetWorkItemKey, or targetProjectId (prj_... or UUID). Also relationType (blocks, depends_on, relates), notes, ownerId. |
PATCH /api/v1/projects/{projectId}/dependencies/{dependencyId} | status, notes. |
GET /api/v1/projects/{projectId}/releases | Returns releases and readiness (see below). |
POST /api/v1/projects/{projectId}/releases | name (required), targetDate, notes. New releases start in planning. |
PATCH /api/v1/projects/{projectId}/releases/{releaseId} | status (planning, blocked, ready, released), notes. |
A dependency's target project must be in the same organization and visible to you; otherwise the request fails with 400. Listed dependencies only expand sourceWorkItem, targetWorkItem, and targetProject for projects you can access. Risk, decision, and dependency changes emit the same metric events as edits in the app.
Release readiness
readiness.ready is true only when all four readiness.gates pass: a goal is active or achieved, an artifact is approved, every policy check passes or has an active exception, and there are no open high-impact risks or unresolved dependencies. A failing gate has a reason, and readiness.blockers lists the risks, dependencies, and policies holding it up. Each release also has canMarkReady, canRelease, and blockedBecause.
Moving a release to ready or released while a gate fails returns 409 CONFLICT with the readiness report in details.readiness.
Attachments
POST /api/projects/{projectId}/attachments
Uploads an image with a multipart/form-data body whose file field holds a PNG, JPEG, GIF, or WebP image. It needs the work_items:write scope, and observers can't upload. The workspace plan sets the largest file you can upload.
curl -X POST "https://skopo.dev/api/projects/prj_S8qDRLbDJWbp/attachments" -H "Authorization: Bearer $SKOPO_TOKEN" -F "[email protected];type=image/png"
Returns 201 with { "id", "url", "markdown" }. The url is a path on the same host, and markdown is an image you can add to a work item's description with PATCH or to a comment. An unsupported file type returns 415. A file above the plan limit returns 413 with code PAYMENT_REQUIRED.
This endpoint shares its path with the browser upload, so errors use the shorter { "error" } shape instead of the /api/v1 error body.
Webhooks
To get changes pushed to you instead of polling, set up webhooks.