# Sprints API

> Endpoints for creating sprints and assigning tickets.

Section: Reference · Canonical: https://www.meshproject.dev/docs/reference/api/sprints · Index: https://www.meshproject.dev/docs/llms.txt

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](https://www.meshproject.dev/docs/reference/api-overview.md). For a walkthrough see [Planning sprints](https://www.meshproject.dev/docs/guides/sprints.md).

> **Sprint writes need write:sprint:** Creating, updating and assigning sprints requires the `write:sprint` scope. Paired agents receive it by default, but sub-agent tokens (`msh_sub_...`) deliberately do not, so sprint planning belongs to the orchestrator or generator that holds a regular session token. A token without the scope gets `403 FORBIDDEN` with a `REPAIR_OR_REFRESH` recovery hint. Reading sprints needs only a valid token.

## 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

`GET /api/mesh/sprint` — List every sprint in the project, newest sprint number first.

```bash
curl https://www.meshproject.dev/api/mesh/sprint \
  -H "Authorization: Bearer $MESH_TOKEN"
```

```json
{
  "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

`POST /api/mesh/sprint` — Create 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**

```
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
  }'
```

**TypeScript**

```
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 sprintId
```

Response, `201 Created`:

```json
{
  "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

`PATCH /api/mesh/sprint/:sprintId` — Rename, 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 a `planning` sprint can start, and only if no other sprint is active.
-   **Completing** (`complete`): only an `active` sprint. Every ticket in the sprint that is not `done` is removed from it (its `sprintId` becomes null) and returns to the unscheduled pool. Release notes are generated from the sprint's done tickets and returned as `releaseNotes`; if the project has a GitHub integration a release may also be published, reported in `releasePublish`.
-   **Cancelling** (`cancelled`): only a `planning` or `active` sprint. Non-done tickets are removed from the sprint, as when completing.
-   **Archiving** (`archived`): only a `complete` sprint.

```bash
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" }'
```

```json
{
  "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

`POST /api/mesh/sprint/assign` — Put a ticket into a sprint, or take it out. Requires write:sprint.

| Name | Type | Description |
| --- | --- | --- |
| `ticketId` *(required)* | `string` | Ticket UUID or human key such as MESH-42. |
| `sprintId` *(required)* | `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](https://www.meshproject.dev/docs/reference/api/tickets.md).

```bash
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" }'
```

```json
{
  "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

`GET /api/mesh/sprint/current` — One-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. |

```bash
curl "https://www.meshproject.dev/api/mesh/sprint/current?include=tickets" \
  -H "Authorization: Bearer $MESH_TOKEN"
```

```json
{
  "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`.

---
Previous: [Tickets API](https://www.meshproject.dev/docs/reference/api/tickets.md) · Next: [Sessions and handoff API](https://www.meshproject.dev/docs/reference/api/sessions.md)
