---
title: REST API
description: The HTTP surface — authentication, the REST endpoints, and where tRPC takes over.
order: 10
---

# REST API

Izri's HTTP surface is deliberately small. REST covers the things that must be plain HTTP — health checks, CI triggers, inbound webhooks, file serving — and everything else goes through tRPC.

For programmatic use, prefer the [CLI](/docs/install/cli) or the [MCP server](/docs/install/mcp). They wrap this surface and stay in step with it.

## Authentication

```
Authorization: Bearer izri_xxxxxxxxxxxx
```

The API checks the `izri_` prefix first and only then validates the token. Without the prefix the request falls through to cookie-session handling and returns an opaque `401`. See [API tokens](/docs/configure/api-tokens).

Browser sessions use a cookie set by the auth flow; token auth takes precedence when both are present.

## Endpoints

### `GET /health`

Liveness. No authentication.

```bash
curl https://api.izri.ai/health
```

### `GET /api/test-runs/:runId/status`

Current status of a test run. Requires `tests:read`.

```bash
curl -H "Authorization: Bearer $IZRI_API_TOKEN" \
     https://api.izri.ai/api/test-runs/01HK.../status
```

Returns one of `PENDING`, `RUNNING`, `PASSED`, `FAILED`, `ERROR`, `CANCELLED`. See [verdicts and exit codes](/docs/reference/verdicts-and-exit-codes).

For polling, prefer `izri run --watch` — it already implements sane intervals and a timeout.

### `POST /api/ci/trigger/:projectId`

Trigger a run from a CI system that isn't GitHub Actions.

```bash
curl -X POST \
     -H "Authorization: Bearer $IZRI_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"branch":"feat/x","commitSha":"abc123"}' \
     https://api.izri.ai/api/ci/trigger/01HK...
```

Requires `tests:run`. Returns the run identifier immediately; the run is asynchronous.

### `POST /api/ci/evidence/:projectId`

Upload a commit's test evidence. Two kinds of upload share the route:

- **Tests evidence** — the JUnit results of your CI run (the [`izri/tests`](/docs/signals/tests) verdict) and optionally a coverage report. `izri upload` sends it.
- **The revert check** — the [revert check](/docs/signals/hallucination#the-revert-check)'s two runs, at head and with the change reverted. `izri verify` sends it. It feeds `izri/hallucination` only, never `izri/tests`, and is judged once tests evidence for the same commit exists.

Requires `tests:run`: what you upload becomes the verdict, so a read-only token is refused with `403`.

From GitHub Actions, send the job's id-token in `X-Izri-OIDC`. The job needs `permissions: id-token: write`; `izri upload` and the Action then request and send the token themselves. Calling the route yourself, request it with the origin of the Izri URL you upload to as the audience (`https://api.izri.ai` here; the API's own origin or the app origin that proxies it both work, your own when self-hosting): `curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.izri.ai" | jq -r .value`. A token minted for any other audience, GitHub's default included, is refused with `401`. Its signed claims bind the upload to the repository, run and workflow file that produced it, GitHub's record of that run binds the commit, and that is what makes the evidence trusted. Without the header the upload is recorded but never trusted.

```bash
curl -X POST \
     -H "Authorization: Bearer $IZRI_API_TOKEN" \
     -H "X-Izri-OIDC: $ACTIONS_ID_TOKEN" \
     -H "Content-Type: application/json" \
     -d @evidence.json \
     https://api.izri.ai/api/ci/evidence/01HK...
```

```json
{
  "baseSha": "…", "headSha": "…",
  "head": { "junit": "<testsuites>…</testsuites>" },
  "coverage": { "format": "lcov", "report": "TN:\nSF:src/tax.ts\n…" },
  "run": { "id": "123456", "workflowPath": ".github/workflows/ci.yml" }
}
```

`head` or `skipped: { "reason": "…" }` is required. `baseSha` is informational: the base Izri compares against is the pull request's own, as GitHub reported it. `coverage.format` is `lcov` or `cobertura`. `run` names the GitHub Actions run that produced the upload; with an id-token Izri confirms the claimed run with GitHub and `run` may be omitted, and without one the upload is recorded as untrusted whatever `run` says. The body is capped at 5 MB.

A revert-check upload carries `head` and `reverted: { "junit" }` with `testsSelected` (the number of test files the change selected), or `skippedReason: "budget"` with `testsSelected` and no results when that selection was over budget. It takes no `coverage` or `skipped`, and `reverted` needs `head`; those combinations are refused with `400`.

| Status | Meaning |
| --- | --- |
| `200` | Recorded. Tests evidence returns `testRunId`, `testsStatus`, `totalTests` and `trusted`. A revert check returns `verificationRunId`, `findings` (categories), `trusted` and `judged`; `judged: false` comes with `notJudgedReason` (for example `no izri/tests evidence for this commit`) and is judged when that evidence arrives. |
| `400` | Invalid body, or unparsable JUnit or coverage. `oidc_mismatch`: the id-token was issued to another repository, or the project's repository is not a GitHub `owner/repo` a token can be matched to. |
| `401` | `oidc_invalid`: the `X-Izri-OIDC` token is not a GitHub Actions id-token for this Izri (bad signature, expired, wrong audience). Nothing is recorded. |
| `409` | `no_delta_for_head` — Izri has no delta for that commit yet. The webhook creates one on push; retry shortly. |
| `413` | Over the size cap. |
| `503` | `oidc_unavailable`: GitHub's signing keys could not be fetched to check the id-token. Nothing is recorded; retry. |

Whether the evidence is trusted is decided from the id-token's claims and the GitHub compare of the PR, never from the body of the upload.

### `GET /files/*`

Serves stored artifacts — visual snapshots, diffs, reports. Authenticated: these are your organization's artifacts, not public objects.

## Inbound webhook receivers

These are endpoints Izri *receives* on. You don't call them; you configure a provider to.

| Endpoint | Sender |
| --- | --- |
| `POST /api/webhooks/github-app` | GitHub App events |
| `POST /api/webhooks/github/:projectId` | Per-project GitHub webhook |
| `POST /api/webhooks/stripe` | Stripe billing events |

For webhooks Izri sends *you*, see [outbound webhooks](/docs/configure/webhooks).

## Authentication routes

`/api/auth/**` is handled by the auth layer — OAuth callbacks, session management. Not intended for direct calls.

The GitHub OAuth callback must exactly match the configured app URL. A port mismatch produces a silent redirect loop rather than an error, and it is the most common self-hosting misconfiguration.

## tRPC

Everything else is tRPC, mounted at `/trpc/*`, with the same bearer-token authentication.

The tRPC surface is typed end-to-end and changes with the product. Rather than documenting procedure signatures that would drift, use the clients that are versioned against it — the [CLI](/docs/install/cli) and the [MCP server](/docs/install/mcp).

## Errors

Standard HTTP status codes.

| Status | Meaning |
| --- | --- |
| `401` | Missing, malformed, or invalid token. Check the `izri_` prefix. |
| `403` | Authenticated, but the token lacks the required scope. |
| `404` | Not found — **or** the resource belongs to another organization. |
| `402` | The operation requires a higher plan. |
| `429` | Rate limited. |

`404` doubling as a cross-organization permission error is intentional: it avoids confirming that a resource exists to someone who can't access it. If you're sure an ID is valid and still get `404`, verify the token belongs to the right organization.

## Related

- [API tokens](/docs/configure/api-tokens)
- [Verdicts and exit codes](/docs/reference/verdicts-and-exit-codes)
