Releases

Releases

List releases, create a release, and place builds into them — everything a CI/CD pipeline needs to line a build up behind a release without touching the UI.

Human-only: the API can create a release and move builds between release buckets, but it can never pass a manual test, complete a release, or delete anything. Those stay in the UI.


Scopes#

ScopeEndpoint
releases:readGET /releases, GET /releases/{id}
releases:createPOST /releases
builds:updatePATCH /builds/{id}

Existing API keys do not gain these scopes automatically — an admin enables them under Org Settings → API Keys.


Project resolution#

There is no default project. A project-scoped key operates on its own project. An org-scoped key must name the project with project_key (project UUID or project_code) on GET /releases and POST /releases, or gets a 400. On ID routes (GET /releases/{id}, PATCH /builds/{id}), the resource's project must simply belong to the key's org — a project-scoped key only ever sees its own project, and anything else is a 404, never a 403.


List releases#

GET /api/v1/releases?project_key=MOB&status=active&limit=20&offset=0

Returns a paginated list, newest first:

{
  "data": [
    {
      "id": "b15c105a-8472-5a71-8d27-299e7d6fb6fe",
      "name": "v2.4 - Spring Release",
      "status": "active",
      "target_date": "2026-09-30",
      "completed_at": null,
      "created_at": "2026-09-01T09:00:00Z",
      "project_id": "6c7fe55a-42b2-5170-b88e-9db7b12d911f"
    }
  ],
  "pagination": { "total": 1, "limit": 20, "offset": 0, "has_more": false }
}

status accepts planned, active, or completed; anything else is a 400.


Get a release#

GET /api/v1/releases/{id}

Returns the release plus a build_count — the number of non-deleted builds currently sitting in it. Nothing else: no update, no status change, no delete.


Create a release#

POST /api/v1/releases
Content-Type: application/json

{
  "project_key": "MOB",
  "name": "v2.5 - Summer Release",
  "description": "Optional",
  "target_date": "2026-10-15",
  "status": "planned"
}
  • status accepts planned (default) or active. completed is refused with 400 VALIDATION_ERROR, details.reason: 'release_status_not_allowed' — creating a release that is already completed is completing one, and completing is human-only.
  • Release names are unique per project (case-insensitive). A duplicate is refused with 409 CONFLICT, details.reason: 'release_name_taken'. Releases are never auto-suffixed — unlike builds.
  • target_date must be today or a future date (same rule as the UI).
  • Success: 201 Created with a Location: /api/v1/releases/{id} header.
  • Retries: send an Idempotency-Key header — an identical retry returns 200 with the original release (no second insert); the same key with a different body returns 409.

Place a build in a release (or back to "No release")#

PATCH /api/v1/builds/{id}
Content-Type: application/json

{ "release_id": "b15c105a-8472-5a71-8d27-299e7d6fb6fe" }

The body is exactly one field — release_id (a release UUID, or null to move the build back to the project's "No release" bucket). Any other field is a 400, and no other build field is editable through the API.

The move behaves exactly like the UI's Place in release:

  • The build's test runs follow it in the database — nothing else to update.
  • A name collision in the target release suffixes the build's name instead of refusing: the response name may come back as "v2.4.1 (2)".
  • A completed release refuses the move in either direction — in or out — with 409 CONFLICT, details.reason: 'release_completed'.
  • A release_id from another project is a 404.

Response (200):

{ "id": "aa7cd7d3-c5e2-56b0-b658-41d41fbc33a2", "name": "v2.4.1", "release_id": "b15c105a-8472-5a71-8d27-299e7d6fb6fe", "status": "active" }

List builds by release bucket#

GET /api/v1/builds?release=none          # only release-less builds
GET /api/v1/builds?release={releaseId}   # only that release's builds

The release filter accepts none (the project's "No release" bucket) or a release UUID — which must belong to the resolved project, else 404. GET /builds also accepts project_key as an alternative to project_id; sending both with different values is a 400.


Errors#

All errors use the standard shape { "error": { "code", "message", "details?" } }. See Error Reference.

StatusWhen
400Missing project_key on an org-scoped key; status: "completed"; past target_date; extra body fields; unknown status filter
403Key lacks releases:read / releases:create / builds:update
404Resource in another project/org; release naming a release outside the resolved project
409Duplicate release name (release_name_taken); completed release (release_completed); Idempotency-Key reuse