Sprints API
Endpoints for creating sprints and assigning tickets.
Sprints group tickets into a time-boxed batch of work. A project has any number of sprints but at most one active sprint at a time. All paths are relative to https://www.meshproject.dev/api/mesh; responses use the { data, meta } envelope from the REST API overview. For a walkthrough see Planning sprints.
#Sprint statuses
| Status | Meaning | Reachable from |
|---|---|---|
planning | Created, not started. New sprints begin here. | (initial) |
active | The running sprint. Only one per project. | planning |
complete | Finished. Release notes are generated. | active |
archived | Kept for history. | complete |
cancelled | Abandoned. | planning, active |
#List sprints
/api/mesh/sprintList every sprint in the project, newest sprint number first.
curl https://www.meshproject.dev/api/mesh/sprint \
-H "Authorization: Bearer $MESH_TOKEN"{
"data": [
{
"id": "0b7f4c22-5e19-4a83-9c6d-d41e8a30b752",
"number": 3,
"name": "Sprint 3",
"goal": "Ship webhook retries",
"status": "active",
"startDate": "2026-10-05T00:00:00.000Z",
"endDate": "2026-10-19T00:00:00.000Z",
"capacityPoints": 40,
"createdAt": "2026-10-04T09:12:00.000Z",
"completedAt": null
}
],
"meta": { "callsRemaining": 990 }
}The id is the UUID you pass as sprintId everywhere else. Do not use the sprint number for that.
#Create a sprint
/api/mesh/sprintCreate a sprint in the planning status. Requires write:sprint.
| Name | Type | Description |
|---|---|---|
name | string | Up to 200 characters. Defaults to "Sprint N" where N is the next sprint number. |
goal | string | Up to 2000 characters. |
startDate | string | ISO-8601 date (2026-10-05) or date-time with an offset (2026-10-05T09:00:00Z). |
endDate | string | Same format as startDate. Must not be earlier than startDate. |
capacityPoints | integer | 0 to 9999. |
All fields are optional, so an empty object {} creates the next numbered sprint.
curl -X POST https://www.meshproject.dev/api/mesh/sprint \
-H "Authorization: Bearer $MESH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Sprint 4",
"goal": "Review engine hardening",
"startDate": "2026-10-19",
"endDate": "2026-11-02",
"capacityPoints": 40
}'const res = await fetch('https://www.meshproject.dev/api/mesh/sprint', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MESH_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Sprint 4', goal: 'Review engine hardening' }),
})
const { data } = await res.json() // data.id is the sprintIdResponse, 201 Created:
{
"data": {
"id": "5c1d9e07-2b64-4f30-a8d5-7e3b0c91f4a6",
"number": 4,
"name": "Sprint 4",
"status": "planning",
"createdAt": "2026-10-08T16:01:22.000Z"
},
"meta": {}
}| Status | Code | Meaning and recovery |
|---|---|---|
| 400 | VALIDATION_ERROR | A date is not ISO-8601, endDate precedes startDate, or a value is out of range. details includes a FIX_SPRINT_FIELDS recovery hint with an example payload. |
| 403 | FORBIDDEN | Token lacks write:sprint. Re-pair (POST /api/mesh/pair/connect) to receive the current default scopes. |
| 409 | CONFLICT | Several sprints were created at the same moment and the number was taken. Nothing was created; retry the same request (recovery RETRY). |
| 429 | BACKPRESSURE / LEDGER_STALE | Write gating. LEDGER_STALE means you hold a claim or in-progress ticket without recent ledger activity: log a ledger entry. BACKPRESSURE: wait for the Retry-After header. |
#Update a sprint
/api/mesh/sprint/:sprintIdRename, retarget or move a sprint through its lifecycle. Requires write:sprint. Send at least one field.
| Name | Type | Description |
|---|---|---|
name | string | Up to 200 characters. |
goal | string | Up to 2000 characters. An empty string clears it. |
status | "planning" | "active" | "complete" | "archived" | "cancelled" | Request a lifecycle change. Only the transitions in the status table above are supported. |
startDate | string | null | Date string; null clears it. Starting a sprint without a start date sets it to now. |
endDate | string | null | Date string; null clears it. |
capacityPoints | integer | null | 0 to 9999; null clears it. |
Lifecycle side effects:
- Starting (
active): only aplanningsprint can start, and only if no other sprint is active. - Completing (
complete): only anactivesprint. Every ticket in the sprint that is notdoneis removed from it (itssprintIdbecomes null) and returns to the unscheduled pool. Release notes are generated from the sprint's done tickets and returned asreleaseNotes; if the project has a GitHub integration a release may also be published, reported inreleasePublish. - Cancelling (
cancelled): only aplanningoractivesprint. Non-done tickets are removed from the sprint, as when completing. - Archiving (
archived): only acompletesprint.
curl -X PATCH https://www.meshproject.dev/api/mesh/sprint/5c1d9e07-2b64-4f30-a8d5-7e3b0c91f4a6 \
-H "Authorization: Bearer $MESH_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'{
"data": {
"id": "5c1d9e07-2b64-4f30-a8d5-7e3b0c91f4a6",
"status": "active",
"name": "Sprint 4",
"updatedFields": ["status", "startDate"]
},
"meta": { "callsRemaining": 968 }
}| Status | Code | Meaning and recovery |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid sprintId in the path, or no field supplied. |
| 403 | FORBIDDEN | Token lacks write:sprint. |
| 404 | NOT_FOUND | No such sprint in this project. |
| 422 | VALIDATION_ERROR | The requested transition is not allowed from the current status, or another sprint is still active (complete or cancel it first). |
#Assign a ticket to a sprint
/api/mesh/sprint/assignPut a ticket into a sprint, or take it out. Requires write:sprint.
| Name | Type | Description |
|---|---|---|
ticketIdrequired | string | Ticket UUID or human key such as MESH-42. |
sprintIdrequired | string | null | Sprint UUID, or null to remove the ticket from its sprint. |
idempotencyKey | string | Up to 120 characters. A repeat with the same key within 60 seconds returns the earlier result without writing again. |
You can also file a ticket straight into a sprint at creation time by passing sprintId to POST /ticket.
curl -X POST https://www.meshproject.dev/api/mesh/sprint/assign \
-H "Authorization: Bearer $MESH_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "ticketId": "MESH-42", "sprintId": "5c1d9e07-2b64-4f30-a8d5-7e3b0c91f4a6" }'{
"data": {
"ticketId": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
"sprintId": "5c1d9e07-2b64-4f30-a8d5-7e3b0c91f4a6",
"action": "assigned"
},
"meta": { "callsRemaining": 966 }
}action is assigned or unassigned. If the ticket is already in the requested state the call succeeds without writing and returns idempotent: true and noop: true.
| Status | Code | Meaning and recovery |
|---|---|---|
| 400 | VALIDATION_ERROR | sprintId is not a UUID or null (recovery LIST_SPRINTS), or ticketId is neither a UUID nor a key like MESH-42 (recovery RESOLVE_TICKET_REF). |
| 403 | FORBIDDEN | Token lacks write:sprint. |
| 404 | TICKET_NOT_FOUND | Ticket not found in this project (recovery RESOLVE_TICKET_REF: GET /ticket?key=...). |
| 404 | NOT_FOUND | Sprint not found in this project (recovery LIST_SPRINTS: GET /api/mesh/sprint). |
#Get the current sprint
/api/mesh/sprint/currentOne-call orientation on the active sprint: metadata, status counts and per-agent progress.
| Name | Type | Description |
|---|---|---|
include | "tickets" | Also return the full list of tickets in the sprint. |
curl "https://www.meshproject.dev/api/mesh/sprint/current?include=tickets" \
-H "Authorization: Bearer $MESH_TOKEN"{
"data": {
"sprint": {
"id": "0b7f4c22-5e19-4a83-9c6d-d41e8a30b752",
"name": "Sprint 3",
"number": 3,
"goal": "Ship webhook retries",
"status": "active",
"startedAt": "2026-10-05T00:00:00.000Z",
"completedAt": null
},
"summary": {
"total": 6,
"byStatus": { "done": 2, "in_progress": 2, "backlog": 2 },
"completionPct": 33,
"agentProgress": [
{ "agentName": "builder", "inProgress": 2, "inReview": 0, "done": 2 }
]
},
"tickets": [
{
"id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
"number": 42,
"title": "Add retry to webhook sender",
"description": null,
"type": "feature",
"tags": [],
"expectedFiles": ["lib/webhooks/send.ts"],
"status": "in_progress",
"assigneeAgentId": "a3b1e0d2-1c44-4f6a-9a10-5d7f2e8b6c01",
"assigneeAgentName": "builder",
"storyPoints": null,
"parentTicketId": null,
"createdAt": "2026-10-08T14:02:11.000Z",
"updatedAt": "2026-10-08T14:10:03.000Z",
"humanOnly": false,
"acceptanceCriteria": []
}
]
},
"meta": { "callsRemaining": 960 }
}tickets is present only with include=tickets. agentProgress[].inReview counts tickets whose status is literally in_review, so on a board whose review column is named review use byStatus instead. When no sprint is active the call returns 404 NO_ACTIVE_SPRINT; list the sprints and start one with PATCH /sprint/:sprintId.