REST API

The plain-REST ingest and read API, authed by an API key. What the reporter, CLI, and MCP server call.

kinora’s public API is plain REST, so any CI provider, curl, or language can push results and read them back. The reporter and CLI call the ingest endpoints for you; the MCP server calls the read endpoints. Use this reference when you want a custom integration.

Base URL

  • Cloud: https://api.kinora.dev
  • Self-host: your server’s origin.

Authentication

Every request needs an API token in the Authorization header:

Authorization: Bearer <token>

Create a token in the dashboard under Settings → Workspace. The token is scoped to the workspace that owns it; all requests read and write only that workspace’s data.

Requests are rate-limited per client IP (INGEST_RATE_LIMIT, default 600/min).

Ingest

POST /api/v1/runs

Upload one normalized run. The project is created automatically the first time its slug is seen. The body is the normalized run JSON that @kinora/core produces (the reporter and CLI build it); its top-level shape:

{
  "project": { "slug": "web-app", "name": "Web App" },
  "run": {
    "startedAt": "2026-07-18T10:00:00.000Z",
    "duration": 42000,
    "counts": { "total": 126, "expected": 120, "unexpected": 2, "flaky": 1, "skipped": 3 },
    "playwrightVersion": "1.50.0",
    "git": { "sha": "", "branch": "main", "baseBranch": "main", "repoUrl": "" },
    "ci": { "provider": "github", "runUrl": "", "runNumber": "42" },
    "shards": 4
  },
  "tests": [{ "testKey": "", "title": "", "status": "expected", "": "" }]
}

Query parameters:

ParamEffect
backfill=1Historical import: suppresses alerts and stays unmetered (billing follows run.startedAt).
regression=1Compute regression vs the base branch and include it in the response.

Response 201:

{ "projectId": "", "runId": "", "tests": 126, "runUrl": "https://app.kinora.dev/projects/web-app/runs/…", "regression": { "": "" } }

runUrl is always returned; regression only when regression=1 was passed.

Errors: 401 invalid API key · 402 plan limit reached (cloud free tier) · 413 body over 25 MB.

POST /api/v1/runs/:runId/artifacts

Upload a trace.zip (or other binary) for a run, as multipart/form-data. Send the file part plus a testKey field to link it to a test, and an optional name field.

Response 201:

{ "url": "https://…/trace.zip" }

Errors: 404 run not found · 400 no file part · 413 file over 100 MB.

Read

All read routes are GET, Bearer-authed, and scoped to your workspace.

GET /api/v1/projects

Every project with its latest run summary.

{ "projects": [{ "id": "web-app", "name": "Web App", "description": "Customer-facing web app", "latestRun": { "": "" } }] }

id is the project slug.

GET /api/v1/projects/:slug/runs

Run summaries, newest first. Query limit (default 50, max 200).

{ "runs": [{ "": "" }] }

GET /api/v1/projects/:slug/runs/:runId

The full report for one run. Use latest as :runId for the most recent run. Returns counts, git/CI metadata, and every test with its status and failures.

GET /api/v1/projects/:slug/failures

The failing and flaky tests of a run (defaults to the latest). Query runId to target a specific run.

{ "runId": "", "startedAt": "", "counts": { "": "" }, "failures": [{ "status": "unexpected", "": "" }] }

GET /api/v1/projects/:slug/history

Per-test history across the project’s runs. Query testKey to narrow to one test; omit it for all.

{ "histories": [{ "testKey": "", "": "" }] }

Examples

Upload a run with curl:

curl -X POST https://api.kinora.dev/api/v1/runs \
  -H "Authorization: Bearer $KINORA_TOKEN" \
  -H "Content-Type: application/json" \
  --data @run.json

Read the latest run’s failures:

curl https://api.kinora.dev/api/v1/projects/web-app/failures \
  -H "Authorization: Bearer $KINORA_TOKEN"