Machine layer

The QA API Beta

Bugs and test cycles for every application under test, callable by machines: CI jobs, crons, dashboards and orchestrated agents. The contract below is generated from the spec this page serves, so the two can never disagree.

Base URL https://miatz.com/api/qa · verify against https://dev.miatz.com/api/qa first.

OpenAPI 3.1 spec (/api/openapi.yaml) — point a client generator or a viewer straight at it.

Authentication

One header, and scopes that never widen themselves

Send the credential as a bearer token. Read never implies write, and the scope is re-checked on every call — a tool list is a convenience, never a boundary.

bearerAuth
Either a `qa_` machine token or a Supabase access token. A browser session cookie also works. 120 requests/minute per credential.
orgSession
The Supabase browser session, which must belong to an owner or admin of the org. The GitHub connection endpoints take no other credential: a grant is repo write access for the whole tenant, so a machine token cannot start or undo one.
  • apps:read
  • bugs:read
  • bugs:write
  • comments:write
  • tests:write
  • github:write
Paging

Every list returns one page

A list that stops at the first page is a silently truncated sample, not an answer. Loop while has_more is true, sending cursor=<next_cursor> and every other parameter unchanged.

count
Rows in this page — never the size of the result set.
has_more
true means matching rows exist that this page did not show.
next_cursor
Send back as ?cursor= for the next page; null on the last.

The cursor is opaque — a keyset position over (sort column, id). Never parse one and never construct your own; a cursor this API did not issue is refused.

Reference

All 19 operations

Read straight from the spec at build time. If an endpoint is missing here, it is missing from the contract.

Apps under test

GET/api/qa/apps

Applications under test this credential can reach

An empty list means nothing is in scope for this credential, never that no apps exist.

query (query, string)
Filter on name or slug
limit (query, integer)
cursor (query, string)
The `next_cursor` from the previous page. Opaque — decode nothing from it and construct none of your own; it encodes a keyset position and the encoding is ours to change. Keep every other filter identical across a walk.
  • 200 · OK
  • 401 · No valid credential
  • 429 · Over 120 requests/minute

Bugs

GET/api/qa/bugs

List bugs, newest first

Use `updated_since` to poll — it returns only what moved. The default excludes closed and won't-fix bugs. Ordered on (created_at desc, id desc) and walked with `cursor`: while `has_more` is true there are older matching bugs this page did not show. A poller that stops at the first page silently loses the rest of its backlog.

app (query, string)
App slug or uuid
workflow_state (query, WorkflowState[])
Repeatable, or comma-separated
severity (query, Severity[])
priority (query, Priority[])
assignee_id (query, string)
mine (query, boolean)
Only bugs the calling person reported. User sessions only.
updated_since (query, string)
include_closed (query, boolean)
limit (query, integer)
cursor (query, string)
The `next_cursor` from the previous page. Opaque — decode nothing from it and construct none of your own; it encodes a keyset position and the encoding is ours to change. Keep every other filter identical across a walk.
  • 200 · OK
  • 401 · No valid credential
  • 403 · The credential is missing a required scope
POST/api/qa/bugs

File a bug

steps, expected and actual are required. A report without them cannot be verified, so it is refused rather than accepted.

Body: NewBug

  • 200 · Filed
  • 400 · The request body failed validation
  • 403 · The credential is missing a required scope
GET/api/qa/bugs/{id}

One bug with its whole comment thread

  • 200 · OK
  • 404 · Not found, or outside this credential's scope (the two are indistinguishable by design)
PATCH/api/qa/bugs/{id}

Move a bug through its lifecycle, or re-grade it

A state change with no `comment` still writes one automatically. A non-staff reporter may send only `comment`.

Body: object

  • 200 · Updated
  • 400 · The request body failed validation
  • 404 · Not found, or outside this credential's scope (the two are indistinguishable by design)
POST/api/qa/bugs/{id}/github

Mirror the bug as a GitHub issue, or attach an existing one

Needs a GitHub repo_url on the app. The credential is resolved per bug, tenant first: (1) the GitHub App installation of the org the bug belongs to, which ALWAYS wins so one customer's connection can never reach another's repos; (2) GITHUB_QA_TOKEN / GITHUB_TOKEN, the platform's own credential, and only for repos owned by GITHUB_QA_PLATFORM_OWNERS — a tenant admin may set repo_url freely, so without that fence they could borrow our access; (3) neither, which is the 503, naming which of the two is missing and for whom. An existing_ref is verified against the GitHub API, not trusted.

Body: object

  • 200 · Linked
  • 400 · The request body failed validation
  • 503 · The GitHub bridge is not configured on this deployment

Comments

GET/api/qa/bugs/{id}/comments

The thread, oldest first

  • 200 · OK
POST/api/qa/bugs/{id}/comments

Append to the thread

Machine comments are attributed to the credential, never to a person.

Body: object

  • 200 · Posted

Test cycles

GET/api/qa/cycles

Recent test cycles, newest first

Ordered on (started_at desc, id desc) and walked with `cursor` while `has_more` is true.

app (query, string)
status (query, CycleStatus[])
limit (query, integer)
cursor (query, string)
The `next_cursor` from the previous page. Opaque — decode nothing from it and construct none of your own; it encodes a keyset position and the encoding is ours to change. Keep every other filter identical across a walk.
  • 200 · OK
POST/api/qa/cycles

Open a test cycle

Body: object

  • 200 · Opened
GET/api/qa/cycles/{id}

One cycle with every case outcome

  • 200 · OK
POST/api/qa/cycles/{id}/results

Post case outcomes, optionally closing the cycle

Totals and the final status are recomputed server-side from the stored rows, never taken from the caller. Any fail or blocked case ⇒ failed.

Body: object

  • 200 · Recorded

GitHub bridge

GET/api/qa/github/connect

Start a tenant's GitHub App installation

A browser redirect, not a machine call: it sends an owner or admin of `orgId` to GitHub's install page for the miatz app, where THEY choose which of their own repositories to grant. The org travels onward inside a signed, expiring `state`, never re-read from the callback's query string.

orgId (query, string, required)
  • 302 · Redirect to GitHub's installation page
  • 400 · The request body failed validation
  • 401 · No valid credential
  • 403 · The credential is missing a required scope
GET/api/qa/github/callback

Where GitHub returns after an install

Called by GitHub, never by an integrator. The signed `state` must verify AND the caller must STILL hold owner/admin on that org; the installation's account is read from GitHub, not from the query string. Every outcome is a redirect to /org/integrations — a JSON body would be shown as raw text in the browser the person landed in.

state (query, string)
The signed
installation_id (query, integer)
setup_action (query, install · update)
`update` with no installation_id is a Configure visit, not a new install
  • 302 · Redirect to /org/integrations — `?github=connected&account=…` when the installation was recorded, `?error=…` otherwise.
DELETE/api/qa/github/installations/{id}

Forget a GitHub connection

Removes OUR record so the bridge stops using it. It deliberately does NOT uninstall the app on GitHub: the grant belongs to the customer's GitHub organization, and revoking it from our side would misrepresent who controls it. The response says where to finish the removal.

orgId (query, string, required)
  • 200 · Forgotten
  • 400 · The request body failed validation
  • 401 · No valid credential
  • 403 · The credential is missing a required scope

MCP

POST/api/qa/mcp

The QA remote MCP server (JSON-RPC 2.0, Streamable HTTP)

Stateless single-response mode. `tools/list` advertises only the tools the calling credential can run, and the dispatcher re-checks the scope before every call — a tool list is a convenience, never the boundary. A TOOL failure comes back 200 with `isError` so the agent can read the reason and adapt; only parse and auth failures use a transport status. Add it to a repo's agent with `claude mcp add --transport http miatz-qa https://miatz.com/api/qa/mcp --header "Authorization: Bearer qa_…"`.

Body: RpcRequest

  • 200 · A JSON-RPC result, including a tool call that failed
  • 202 · A notification was accepted; there is no body
  • 400 · Unparseable JSON-RPC, or an unknown method
  • 401 · No valid credential (JSON-RPC error -32001)
  • 429 · Over 120 requests/minute (JSON-RPC error -32002)

Credentials

GET/api/qa/tokens

Every QA credential, without its hash

Super admin only.

  • 200 · OK
  • 403 · The credential is missing a required scope
POST/api/qa/tokens

Mint a credential

Super admin only. The plaintext is returned EXACTLY ONCE and cannot be recovered. created_by is who machine-filed bugs are attributed to.

Body: object

  • 200 · Minted
DELETE/api/qa/tokens/{id}

Revoke a credential (soft — attribution survives)

  • 200 · Revoked
  • 403 · The credential is missing a required scope
Errors

The whole failure vocabulary

Every response is { "ok": true, … } or { "error": "…" } with a status. Provider text is never echoed back to you.

401 · Unauthorized
No valid credential
403 · Forbidden
The credential is missing a required scope
404 · NotFound
Not found, or outside this credential's scope (the two are indistinguishable by design)
400 · Invalid
The request body failed validation
429 · RateLimited
Over 120 requests/minute

Closed vocabularies

Severity
low · medium · high · critical
Priority
p0 · p1 · p2 · p3
WorkflowState
open · triaged · in_progress · fixed · verified · closed · wont_fix · duplicate · cannot_reproduce
TriageStatus
pending · valid · invalid · duplicate
CycleKind
manual · ci · agent · regression · smoke · e2e
CycleStatus
running · passed · failed · cancelled
CaseResult
pass · fail · skip · blocked
MCP

Add the server to any repo's agent

The same surface, spoken as tools. tools/list advertises only what the calling credential can run, and the dispatcher re-checks the scope before every call.

claude mcp add --transport http miatz-qa \
  https://miatz.com/api/qa/mcp \
  --header "Authorization: Bearer qa_…"

Or commit it per repo, with the token coming from the environment:

{
  "mcpServers": {
    "miatz-qa": {
      "type": "http",
      "url": "https://miatz.com/api/qa/mcp",
      "headers": { "Authorization": "Bearer ${MIATZ_QA_TOKEN}" }
    }
  }
}

Wire your CI to it

Mint a credential from the QA console, give the poller read-only scopes, and verify on dev.miatz.com before you point production at it.