# Milestones API

> Create, list, get, update, and delete project-scoped milestones over the integrator API.

Milestones are project-scoped planning units that group test runs and track release progress. Each milestone has a server-assigned ULID, a readable per-project ID (`M-3`), a status lifecycle (`upcoming` → `active` → `completed` | `archived`), optional date fields, and an optional parent for 1-level hierarchies.

## Create

`POST /api/v1/projects/{projectId}/milestones`

`{projectId}` is the project **code** (for example `ACME`).

### Request body

| Field         | Required | Notes                                                                                     |
| ------------- | -------- | ----------------------------------------------------------------------------------------- |
| `name`        | yes      | Non-empty string, max 255 characters. Must be unique among live milestones in the project |
| `description` | no       | Free-text description, max 2000 characters                                                |
| `status`      | no       | One of `upcoming`, `active`, `completed`, `archived`. Defaults to `upcoming`              |
| `startAt`     | no       | Unix epoch milliseconds. Must be ≤ `dueAt` when both are provided                         |
| `dueAt`       | no       | Unix epoch milliseconds                                                                   |
| `forecastAt`  | no       | Unix epoch milliseconds                                                                   |
| `parentId`    | no       | ULID of the parent milestone in the same project (1 level only)                           |

Client-supplied `ulid` fields are rejected.

Duplicate live name within the project returns `409 conflict`. Date ordering violation (`dueAt < startAt`) or missing required fields return `422 validation_failed`.

### Response

`201` with the created milestone object. See [Response fields](#response-fields).

### Example

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"v2.0 Release","status":"upcoming","startAt":1700000000000,"dueAt":1700600000000}' \
  "https://app.probara.net/api/v1/projects/ACME/milestones"
```

## List

`GET /api/v1/projects/{projectId}/milestones`

Returns the live (non-deleted) milestones for the project, ordered by ULID ascending. Page-based pagination.

### Query parameters

| Parameter  | Notes                                                                     |
| ---------- | ------------------------------------------------------------------------- |
| `page`     | Page number (1-based, default `1`)                                        |
| `pageSize` | Items per page (default `50`, max `200`)                                  |
| `q`        | Case-insensitive substring search on milestone name                       |
| `status`   | Filter by exact status: `upcoming`, `active`, `completed`, or `archived`  |
| `view`     | Response shape: `flat` (default) or `tree`. See [List views](#list-views) |

### List views

The `view` parameter selects the response shape. It is additive — omitting it (or passing `flat`) returns the exact same response as before.

| `view`           | `items`                                                                                                     | `total`                                                          | Pagination unit      |
| ---------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------- |
| `flat` (default) | Every live milestone (parents and children as siblings)                                                     | Count of all live milestones                                     | Individual milestone |
| `tree`           | Parent milestones only (those with no parent), each carrying a `children` array of its live direct children | Count of **parent** milestones only (children are never counted) | Parent milestone     |

In `tree` mode:

- `items` contains only parents. Each parent item is the standard milestone object plus a `children` array.
- Each entry in `children` is a plain milestone object (the same [response shape](#response-fields)); children never carry their own `children` key (the hierarchy is one level).
- `q` and `status` filter **parents only**. A parent matched by the filter shows all of its live children regardless of the children's own status.
- `total` is the parent count, so the pager (`X–Y of Z`) counts parent sections. A parent and its children are never split across pages.
- A parent with no children has `children: []`.

### Response

`200` with `{ items, page, pageSize, total }`. In `flat` mode each item follows the [response shape](#response-fields); in `tree` mode each item is that shape extended with a `children` array (see [List views](#list-views)).

### Example

```bash
# Flat (default)
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones?status=active"

# Grouped parent sections with nested children
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones?view=tree"
```

Example `tree` response (truncated):

```json
{
  "items": [
    {
      "ulid": "01JPARENT...",
      "name": "v2.4 Release",
      "status": "active",
      "children": [{ "ulid": "01JCHILD...", "name": "Auth 2.4", "status": "active" }]
    }
  ],
  "page": 1,
  "pageSize": 50,
  "total": 1
}
```

## Get

`GET /api/v1/milestones/{milestoneUlid}`

Returns a single live milestone scoped to the active organization, including real-time progress aggregates.

Returns `404 not_found` when the milestone is outside the active organization or has been deleted.

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Get by display ID

`GET /api/v1/projects/{projectId}/milestones/by-display-id/{displayId}`

Returns the same milestone object as `GET /api/v1/milestones/{milestoneUlid}`, addressed by its readable ID. `{projectId}` is the project **code** and scopes the lookup; `{displayId}` is `M-{milestoneNumber}` (for example `M-3`) with no project prefix.

- `422 validation_failed` — malformed ID (`M-0`, `M-01`, `M-x`) or another resource's prefix (`R-3`).
- `404 not_found` — no milestone with that number in this project, or the milestone has been deleted.

Milestone numbers start at 1, increase with every milestone created in the project, and are never reused. The web app uses the same ID in its URLs, for example `/projects/ACME/milestones/M-3`.

### Example

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/milestones/by-display-id/M-3"
```

## Update

`PATCH /api/v1/milestones/{milestoneUlid}`

Partial update. At least one field must be provided; an empty body returns `422 validation_failed`.

### Request body

| Field         | Required | Notes                                                                                           |
| ------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `name`        | no       | Non-empty string, max 255 characters                                                            |
| `description` | no       | Max 2000 characters, or `null` to clear                                                         |
| `status`      | no       | Target status. Must follow valid status transitions (see [Status lifecycle](#status-lifecycle)) |
| `startAt`     | no       | Unix epoch milliseconds, or `null` to clear                                                     |
| `dueAt`       | no       | Unix epoch milliseconds. Must be ≥ `startAt` when both are set                                  |
| `forecastAt`  | no       | Unix epoch milliseconds, or `null` to clear                                                     |
| `parentId`    | no       | ULID of the new parent in the same project, or `null` to detach                                 |

Invalid status transitions return `422 validation_failed`. Duplicate live name returns `409 conflict`. Returns `404 not_found` when the milestone is outside the active organization.

### Example

```bash
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"active","dueAt":1700700000000}' \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Delete

`DELETE /api/v1/milestones/{milestoneUlid}`

Soft-deletes the milestone. Returns `204` on success. Subsequent `GET` returns `404 not_found` and the milestone is excluded from list results.

Deletion atomically unassigns all linked test runs, test cases, and child milestones.

Returns `404 not_found` when the milestone is outside the active organization.

### Example

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/milestones/01JXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Status lifecycle

Milestones follow a directed state machine. Only the transitions listed below are accepted; all others return `422 validation_failed`.

| From        | Allowed transitions           |
| ----------- | ----------------------------- |
| `upcoming`  | `active`, `archived`          |
| `active`    | `completed`, `archived`       |
| `completed` | `active` (reopen), `archived` |
| `archived`  | — (terminal)                  |

Transitioning to `completed` automatically sets `completedAt` to the current timestamp. Transitioning away from `completed` clears `completedAt`.

## Response fields

All mutating endpoints (`POST`, `PATCH`) and `GET /api/v1/milestones/{milestoneUlid}` return a single milestone object.

| Field                      | Type                  | Notes                                                                                 |
| -------------------------- | --------------------- | ------------------------------------------------------------------------------------- |
| `ulid`                     | string (ULID)         | Server-assigned unique identifier                                                     |
| `projectId`                | string                | The project code this milestone belongs to                                            |
| `milestoneNumber`          | number                | Per-project number; starts at 1, never reused                                         |
| `displayId`                | string                | Readable ID `M-{milestoneNumber}` (e.g. `M-3`), used in lookups and web URLs          |
| `name`                     | string                | Display name                                                                          |
| `description`              | string \| null        | Optional description                                                                  |
| `status`                   | string                | Current status: `upcoming`, `active`, `completed`, or `archived`                      |
| `startAt`                  | number \| null        | Unix epoch milliseconds                                                               |
| `dueAt`                    | number \| null        | Unix epoch milliseconds                                                               |
| `forecastAt`               | number \| null        | Unix epoch milliseconds                                                               |
| `completedAt`              | number \| null        | Set automatically when status transitions to `completed`                              |
| `parentId`                 | string (ULID) \| null | Parent milestone ULID, or `null` for top-level                                        |
| `progress.totalRuns`       | number                | Total test runs linked to this milestone                                              |
| `progress.closedRuns`      | number                | Closed (finished) test runs                                                           |
| `progress.percentComplete` | number                | `closedRuns / totalRuns × 100`, rounded; `0` when no runs                             |
| `progress.passRate`        | number                | `passed / totalResults × 100`, rounded; `0` when no results                           |
| `progress.passed`          | number                | Test case results that passed across all linked runs                                  |
| `progress.failed`          | number                | Test case results that failed                                                         |
| `progress.blocked`         | number                | Test case results that were blocked                                                   |
| `progress.skipped`         | number                | Test case results that were skipped                                                   |
| `progress.untested`        | number                | Test cases not yet executed                                                           |
| `author`                   | object \| null        | User who created the milestone, or `null` for API-token-created and legacy milestones |
| `author.kind`              | string                | Always `"user"` when present                                                          |
| `author.ulid`              | string (ULID)         | User identifier                                                                       |
| `author.displayName`       | string                | Full name or email of the author                                                      |
| `author.avatarKey`         | string \| null        | Storage key for the author's avatar image                                             |
| `childCount`               | number                | Number of direct child milestones                                                     |
| `testCaseCount`            | number                | Number of test cases linked to this milestone                                         |
| `createdAt`                | number                | Unix epoch milliseconds                                                               |
| `updatedAt`                | number                | Unix epoch milliseconds                                                               |

## Audit events

Milestone mutations emit the following events on the organization activity feed:

| Action                     | Trigger                        |
| -------------------------- | ------------------------------ |
| `milestone.created`        | Milestone created              |
| `milestone.updated`        | Any field updated via PATCH    |
| `milestone.status_changed` | Status field changed via PATCH |
| `milestone.deleted`        | Milestone soft-deleted         |

See [Audit events](https://docs.probara.net/en/developers/api/audit-events/).
