Programmatic API

A read-only REST API for pulling your own project's run history, evidence, release readiness, gate history, flag blast radius and trends into your own systems — an ongoing, machine-token-authenticated counterpart to the dashboard. Team tier and above.

1. Issue a read-only token

On a project's Settings page, click Issue read-only API token. It is shown once — store it as a secret. It can never upload a report and is rejected by the ingest API; an ordinary (write-scoped) CLI token is likewise rejected here.

2. Authenticate

Authorization: Bearer <your read-only token>

Every request below requires this header. A missing, invalid, or wrong-purpose token is rejected before any data is returned.

3. List run history

GET /api/v1/projects/{projectId}/reports
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/reports?limit=50&offset=0"
200 response
{
  "items": [
    {
      "id": "b6e2...",
      "kind": "case",
      "caseId": "CASE-1",
      "oracleLocator": "tickets/CASE-1",
      "passed": true,
      "classification": null,
      "outcome": null,
      "release": "4.2",
      "uploadedAt": "2026-09-09T12:00:00Z",
      "source": "case",
      "flakiness": "Stable"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
  • kind is "case" or "flag-proof"; classification is case-only, outcome is flag-proof-only.
  • limit defaults to 50, maximum 200; offset defaults to 0. Items are newest first.
  • The projectId in the path must match the token's own project — any other id returns 404.
  • source is "case" for an authored case or "junit" for an imported result; an imported row has no oracle reference, so its oracleLocator is null rather than blank.
  • flakiness is "Stable", "Flaky" or "Recovering". It describes the case, not this one report, so every report for a flaky case carries it.

4. Get a report's evidence metadata

GET /api/v1/projects/{projectId}/reports/{reportId}/evidence
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/reports/<reportId>/evidence"
200 response
{
  "reportId": "b6e2...",
  "reportKind": "case",
  "caseId": "CASE-1",
  "passed": true,
  "screenshotIds": ["a1b2c3..."],
  "uploadedAt": "2026-09-09T12:00:00Z",
  "redacted": true
}

Metadata only — no image bytes are ever embedded in this response. Every stored evidence document is already redacted by the uploading CLI before it reaches ReleaseTwin (see Security & credentials), so redacted is always true.

5. Get one report

GET /api/v1/projects/{projectId}/reports/{reportId}
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/reports/<reportId>"

Returns the same object the list route returns for that report. A report id belonging to another project returns 404 — indistinguishable from one that does not exist, so it cannot be used to probe for other projects.

6. Fetch a screenshot

GET /api/v1/projects/{projectId}/evidence-screenshots/{screenshotId}?reportId=
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/evidence-screenshots/<screenshotId>?reportId=<reportId>" \
  --output screenshot.png

Responds with image/png bytes. This is the separate, independently authenticated request the evidence-metadata response above deliberately does not save you: metadata never embeds image bytes. reportId is required — it is what proves the screenshot belongs to an evidence document in this project.

7. Release readiness

GET /api/v1/projects/{projectId}/releases/{label}?window=14d
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/releases/2026.9.1?window=14d"
200 response
{
  "release": "2026.9.1",
  "headline": "Incomplete",
  "greenCount": 3,
  "failingCount": 1,
  "staleCount": 1,
  "flakyCount": 1,
  "windowDays": 14,
  "incompleteReason": "Flaky",
  "cases": [
    { "caseId": "CHECKOUT-SMOKE", "state": "Green", "latestOutcome": "passed", "latestReportAt": "2026-09-11T10:00:00+00:00" },
    { "caseId": "CHECKOUT-FLAKE", "state": "Flaky", "latestOutcome": "failed", "latestReportAt": "2026-09-11T10:00:00+00:00" }
  ]
}
  • headline is "Proven", "NotProven" or "Incomplete". A flaky case cannot make a release Proven; incompleteReason says whether a flaky or a stale case is holding it back, and is null for every other headline.
  • window accepts 7d, 14d, 30d or 90d and defaults to 14d; anything else returns 400.
  • One computed document per release, so this route is not paged.

8. Compare two releases

GET /api/v1/projects/{projectId}/releases/diff?from=&to=
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/releases/diff?from=2026.9.0&to=2026.9.1"
200 response
{
  "from": "2026.9.0",
  "to": "2026.9.1",
  "regressedCount": 1,
  "recoveredCount": 1,
  "unchangedCount": 1,
  "flakyCount": 1,
  "cases": [
    { "caseId": "CHECKOUT-SMOKE", "from": "Green", "to": "Failing", "changeKind": "regressed" },
    { "caseId": "CHECKOUT-NEW", "from": "NoSignal", "to": "Green", "changeKind": "no-signal" }
  ]
}

Each side is "Green", "Failing" or "NoSignal" — a time-independent comparison, so there is no Stale here. changeKind is regressed, recovered, unchanged, no-signal or flaky: a flip by a flaky case is not attributable to either release. Both from and to are required.

9. Flag-proof gate history

GET /api/v1/projects/{projectId}/flag-proof-gate/history
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/flag-proof-gate/history?limit=50&offset=0"
200 response
{
  "items": [
    {
      "id": "3333...",
      "release": "2026.9.1",
      "flagKeys": ["checkout-v2"],
      "verdict": "pass",
      "flagStates": ["checkout-v2:on"],
      "checkedAt": "2026-09-11T10:00:00+00:00"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

The recorded verdicts, newest first. This reads history — running the gate is a write, and happens in CI. Paged like the report list.

10. Flag blast radius

GET /api/v1/projects/{projectId}/flags/{flagKey}
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/flags/checkout-v2"
200 response
{
  "flagKey": "checkout-v2",
  "items": [
    { "caseId": "CHECKOUT-SMOKE", "state": "Green", "tracker": "jira", "ticketKey": "CHK-1" },
    { "caseId": "CHECKOUT-ORPHAN", "state": null, "tracker": null, "ticketKey": null }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
  • Scoped to this token's project. The dashboard shows the blast radius across every project in the organization; a read-only token reaches one project, so this route answers for that project alone.
  • state is null when the case's most recent flag-proof report carries no release label, and tracker/ticketKey are both null when its oracle locator does not resolve to a connected tracker.

11. Trends

GET /api/v1/projects/{projectId}/trends?window=30d
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.releasetwin.com/api/v1/projects/<projectId>/trends?window=30d"
200 response
{
  "window": "ThirtyDays",
  "granularity": "day",
  "buckets": [
    { "start": "2026-09-11T10:00:00+00:00", "casePassRate": 0.75, "flagProofPassRate": 1, "runVolume": 4, "classificationBreakdown": { "ProductBug": 1 } },
    { "start": "2026-09-12T10:00:00+00:00", "casePassRate": null, "flagProofPassRate": null, "runVolume": 0, "classificationBreakdown": {} }
  ],
  "flakiestCases": [
    { "caseId": "CHECKOUT-FLAKE", "flipCount": 4, "lastActivity": "2026-09-11T10:00:00+00:00", "flakiness": "Flaky" }
  ]
}
  • A rate is null, never 0, when its denominator is zero — plot it as a gap, because 0 would read as “everything failed”.
  • window accepts 7d, 30d or 90d and defaults to 30d. 90-day windows bucket by ISO week, shorter ones by UTC day — granularity says which.
  • flipCount is the windowed raw signal, while flakiness comes from the case's whole history, so the two can legitimately disagree.

Plan entitlements

Every route needs the programmaticApiAccess entitlement a read-only token comes with. Sections 7 and 8 additionally need releaseRollup, section 9 flagProofMergeGate, section 10 flagBlastRadiusView, and section 11 trendAnalytics. Access to the API is not access to every feature in it, so a plan without release rollups cannot read them here either.

403 response
{ "error": "entitlement-required", "entitlement": "releaseRollup" }

The refusal names the feature's entitlement, not programmaticApiAccess, which by that point has already passed — so the key in the response is the one to go and change.

Rate limits

Sized for a customer's own polling job, not a CI burst. An over-limit token receives a 429 with a Retry-After header; other tokens are unaffected.

Next

MCP server — the same routes, wired into an AI coding agent so it can read your evidence directly.

Hosted platform — issuing tokens and what the dashboard shows you directly.