API reference
The qaitai REST API (/api/v1), generated from its OpenAPI 3.1 document.
Start runs from CI or scripts, follow them and read the findings. Every instance serves this description at
/api/v1/openapi.json (OpenAPI 3.1): feed it to your client generator.
For GitHub Actions use qaitai/action@v1, for other CI qaitai run --instance.
Authentication
Create a workspace API key in Settings → API keys (admins only) and send it as a bearer token:
curl "$QAITAI_INSTANCE/api/v1/runs?limit=5" -H "Authorization: Bearer $QAITAI_API_KEY"- A key belongs to one workspace and only ever sees that workspace's data. Keys are shown once and stored as SHA-256 hashes.
- Each operation needs one scope (listed below). A key without it gets
403; a missing, unknown or revoked key gets401. - Runs started with a key are ordinary runs: the same domain verification, plan concurrency, allowance, spend caps and BYOK rules as in the app. They show
api_key:<id>as their creator and in the audit log. - The API is for servers and CI: responses carry no CORS headers, so browsers on other sites can't call it with your key.
Conventions
- Errors are JSON:
{ "error": { "code": "run_rejected", "message": "…", "reason": "domain_unverified" } }.messageis safe to show;issueslists field problems forinvalid_request. - Lists are newest first. Pass
limit(1–100, default 20) and the previous page'snextCursorascursor;nextCursorisnullon the last page. - Idempotency: send
Idempotency-KeyonPOST /runsandPOST /suites/{suiteId}/run. A retry with the same key within 24 hours returns the first run (200,Idempotent-Replayed: true) instead of starting and billing another; the same key with a different request is409. - Rate limits per key and minute: Free 60, Pro 300, Team 600, Enterprise and self-hosted 1,200 (self-hosters can set
QAITAI_API_RATE_LIMIT). Responses carryX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset;429comes withRetry-After. Limits are counted per web replica.
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | Malformed JSON, invalid field, bad cursor or limit |
unauthorized | 401 | Missing, unknown or revoked key |
forbidden | 403 | The key lacks the operation's scope |
not_found | 404 | No such resource in the key's workspace |
conflict | 409 | Run already finished, or Idempotency-Key reused for another request |
run_rejected | 422 | Refused by policy; reason is domain_unverified, concurrency_limit, plan_limit, private_host, byok_required, no_url, queue_unavailable or rejected |
rate_limited | 429 | Too many requests; wait Retry-After seconds |
Scopes
| Scope | Allows |
|---|---|
issues:read | GET /issues, GET /issues/{issueId} |
projects:read | GET /projects |
runs:read | GET /runs, GET /runs/{runId}, GET /runs/{runId}/events |
runs:write | POST /runs, POST /runs/{runId}/cancel, POST /suites/{suiteId}/run |
suites:read | GET /suites, GET /suites/{suiteId} |
suites:write | POST /suites |
Runs
Start, follow and cancel runs.
Start a run
POST /api/v1/runs · scope runs:write
Same checks as starting a run in the app: the project must be in the key's workspace, cloud runs on qaitai Cloud need a verified domain, and plan concurrency, allowance and spend caps apply. Policy refusals are 422 run_rejected with a reason.
| Parameter | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string ≤255 | Retries with the same key within 24 hours return the first run (200, Idempotent-Replayed: true) instead of starting another. Reusing a key for a different request is a 409. |
Body (application/json):
| Field | Type | Description |
|---|---|---|
url | string ≤2048 (uri) | Start URL. Cloud runs need a verified domain on qaitai Cloud. |
prompt | string ≤8000 | The journey to test, in plain English (10-8000 characters). |
projectId (optional) | string ≤64 | Project to file the run under. Optional when the workspace has one project, or one whose app URL has the same host as url. |
workers (optional) | integer 1–10 | Parallel workers (default 1). |
model (optional) | string ≤100 | Model id (default claude-sonnet-5). |
keyMode (optional) | "managed" | "byok" | Default: byok when the workspace has a model key connected, else managed. |
runtime (optional) | "cloud" | "self_hosted" | Default: self_hosted for private/localhost URLs, else cloud. |
gitRef (optional) | string ≤200 | e.g. "main@a41e2d" or "pr/412". |
Response 201: Run. The queued run. 200 when an Idempotency-Key replay returns an earlier run.
Errors: 400, 401, 403, 404, 409, 422, 429.
curl -X POST "$QAITAI_INSTANCE/api/v1/runs" \
-H "Authorization: Bearer $QAITAI_API_KEY" \
-H "Idempotency-Key: $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
-H "Content-Type: application/json" \
-d '{"url":"https://staging.acme.dev","prompt":"Sign up, create a project and invite a teammate"}'List runs
GET /api/v1/runs · scope runs:read
| Parameter | In | Type | Description |
|---|---|---|---|
projectId | query | string | Only this project's runs. |
suiteId | query | string | Only this suite's runs. |
status | query | RunStatus | Only runs with this status. |
cursor | query | string | nextCursor from the previous page. |
limit | query | integer 1–100 | Page size, 1-100 (default 20). |
Response 200: RunPage. Newest first.
Errors: 400, 401, 403, 429.
curl "$QAITAI_INSTANCE/api/v1/runs" \
-H "Authorization: Bearer $QAITAI_API_KEY"Get a run
GET /api/v1/runs/{runId} · scope runs:read
The run with its workers and findings. Poll this or stream /events until status is terminal.
| Parameter | In | Type | Description |
|---|---|---|---|
runId | path | string ≤64 | The run id. |
Response 200: RunDetail. The run.
Errors: 401, 403, 404, 429.
curl "$QAITAI_INSTANCE/api/v1/runs/<runId>" \
-H "Authorization: Bearer $QAITAI_API_KEY"Cancel a run
POST /api/v1/runs/{runId}/cancel · scope runs:write
| Parameter | In | Type | Description |
|---|---|---|---|
runId | path | string ≤64 | The run id. |
Response 200: Run. The cancelled run.
Errors: 401, 403, 404, 409, 429.
curl -X POST "$QAITAI_INSTANCE/api/v1/runs/<runId>/cancel" \
-H "Authorization: Bearer $QAITAI_API_KEY"Stream run events (SSE)
GET /api/v1/runs/{runId}/events · scope runs:read
Server-sent events until the run finishes: run {id, status, startedAt, finishedAt} on change, worker {id, label, status, startedAt, finishedAt} on change, step {id, runWorkerId, index, kind, summary, at} (the SSE id is its timestamp), issue {id, severity, title, stepId, createdAt}, then end {}. The server closes the stream after 10 minutes; reconnect with Last-Event-ID to resume without repeating steps.
| Parameter | In | Type | Description |
|---|---|---|---|
runId | path | string ≤64 | The run id. |
Last-Event-ID | header | string | Resume after this step. |
Response 200: text/event-stream. An event stream.
Errors: 401, 403, 404, 429.
curl -N "$QAITAI_INSTANCE/api/v1/runs/<runId>/events" \
-H "Authorization: Bearer $QAITAI_API_KEY"Suites
Saved journeys you can run on demand or on a schedule.
List suites
GET /api/v1/suites · scope suites:read
| Parameter | In | Type | Description |
|---|---|---|---|
projectId | query | string | Only this project's suites. |
cursor | query | string | nextCursor from the previous page. |
limit | query | integer 1–100 | Page size, 1-100 (default 20). |
Response 200: SuitePage. Newest first.
Errors: 400, 401, 403, 429.
curl "$QAITAI_INSTANCE/api/v1/suites" \
-H "Authorization: Bearer $QAITAI_API_KEY"Create a suite
POST /api/v1/suites · scope suites:write
Body (application/json):
| Field | Type | Description |
|---|---|---|
projectId | string ≤64 | a string at most 64 character(s) long |
name | string ≤80 | a string at most 80 character(s) long |
prompt | string ≤8000 | The journey to test, in plain English. |
triggers (optional) | "pr" | "deploy" | "nightly" | "weekly" | "manual"[] | Default ["manual"]. |
mode (optional) | "exploratory" | "replay" | Default exploratory. |
workerCount (optional) | integer 1–10 | a number between 1 and 10 |
scheduleHour (optional) | integer 0–23 | a number between 0 and 23 |
scheduleWeekday (optional) | integer 0–6 | a number between 0 and 6 |
timezone (optional) | string | |
rerunOnFailure (optional) | boolean | |
enabled (optional) | boolean |
Response 201: Suite. The suite.
Errors: 400, 401, 403, 404, 429.
curl -X POST "$QAITAI_INSTANCE/api/v1/suites" \
-H "Authorization: Bearer $QAITAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"projectId":"<projectId>","name":"<name>","prompt":"Sign up, create a project and invite a teammate"}'Get a suite
GET /api/v1/suites/{suiteId} · scope suites:read
| Parameter | In | Type | Description |
|---|---|---|---|
suiteId | path | string ≤64 | The suite id. |
Response 200: Suite. The suite.
Errors: 401, 403, 404, 429.
curl "$QAITAI_INSTANCE/api/v1/suites/<suiteId>" \
-H "Authorization: Bearer $QAITAI_API_KEY"Run a suite
POST /api/v1/suites/{suiteId}/run · scope runs:write
Starts the suite now, against url or the project's app URL: a replay when the suite is compiled, else an exploratory run. Needs runs:write because it starts a run.
| Parameter | In | Type | Description |
|---|---|---|---|
suiteId | path | string ≤64 | The suite id. |
Idempotency-Key | header | string ≤255 | Retries with the same key within 24 hours return the first run (200, Idempotent-Replayed: true) instead of starting another. Reusing a key for a different request is a 409. |
Body (application/json):
| Field | Type | Description |
|---|---|---|
url (optional) | string ≤2048 (uri) | Run against this URL instead of the project's app URL (e.g. a preview deploy). |
gitRef (optional) | string ≤200 | a string at most 200 character(s) long |
Response 201: Run. The queued run.
Errors: 400, 401, 403, 404, 409, 422, 429.
curl -X POST "$QAITAI_INSTANCE/api/v1/suites/<suiteId>/run" \
-H "Authorization: Bearer $QAITAI_API_KEY" \
-H "Idempotency-Key: $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
-H "Content-Type: application/json" \
-d '{}'Issues
Findings reported by workers.
List issues
GET /api/v1/issues · scope issues:read
| Parameter | In | Type | Description |
|---|---|---|---|
runId | query | string | Only findings of this run. |
projectId | query | string | Only this project's findings. |
severity | query | Severity | Only this severity. |
status | query | string | e.g. "open". |
cursor | query | string | nextCursor from the previous page. |
limit | query | integer 1–100 | Page size, 1-100 (default 20). |
Response 200: IssuePage. Newest first.
Errors: 400, 401, 403, 429.
curl "$QAITAI_INSTANCE/api/v1/issues" \
-H "Authorization: Bearer $QAITAI_API_KEY"Get an issue
GET /api/v1/issues/{issueId} · scope issues:read
| Parameter | In | Type | Description |
|---|---|---|---|
issueId | path | string ≤64 | The issue id. |
Response 200: Issue. The issue.
Errors: 401, 403, 404, 429.
curl "$QAITAI_INSTANCE/api/v1/issues/<issueId>" \
-H "Authorization: Bearer $QAITAI_API_KEY"Projects
Apps under test.
List projects
GET /api/v1/projects · scope projects:read
| Parameter | In | Type | Description |
|---|---|---|---|
cursor | query | string | nextCursor from the previous page. |
limit | query | integer 1–100 | Page size, 1-100 (default 20). |
Response 200: ProjectPage. Newest first.
Errors: 400, 401, 403, 429.
curl "$QAITAI_INSTANCE/api/v1/projects" \
-H "Authorization: Bearer $QAITAI_API_KEY"Schemas
Error
| Field | Type | Description |
|---|---|---|
error | object |
ErrorCode
"invalid_request" | "unauthorized" | "forbidden" | "not_found" | "conflict" | "run_rejected" | "payload_too_large" | "rate_limited" | "internal_error" | "unavailable"
Run
| Field | Type | Description |
|---|---|---|
id | string | |
projectId | string | |
suiteId | string | null | |
kind | "agent" | "replay" | |
status | RunStatus | |
prompt | string | |
targetUrl | string | |
gitRef | string | null | |
workerCount | number | |
maxSteps | number | Step limit per worker. |
maxMinutes | number | Time limit per worker. |
runtime | "cloud" | "self_hosted" | |
keyMode | "managed" | "byok" | |
model | string | |
createdBy | string | Member id, "github:<login>", "schedule:<trigger>" or "api_key:<id>". |
rerunOf | string | null | |
createdAt | string (date-time) | |
startedAt | string (date-time) | null | |
finishedAt | string (date-time) | null | |
url | string | The run in the qaitai app. |
RunStatus
"queued" | "running" | "passed" | "failed" | "flaky" | "cancelled" | "errored"
CreateRunRequest
| Field | Type | Description |
|---|---|---|
url | string ≤2048 (uri) | Start URL. Cloud runs need a verified domain on qaitai Cloud. |
prompt | string ≤8000 | The journey to test, in plain English (10-8000 characters). |
projectId (optional) | string ≤64 | Project to file the run under. Optional when the workspace has one project, or one whose app URL has the same host as url. |
workers (optional) | integer 1–10 | Parallel workers (default 1). |
model (optional) | string ≤100 | Model id (default claude-sonnet-5). |
keyMode (optional) | "managed" | "byok" | Default: byok when the workspace has a model key connected, else managed. |
runtime (optional) | "cloud" | "self_hosted" | Default: self_hosted for private/localhost URLs, else cloud. |
gitRef (optional) | string ≤200 | e.g. "main@a41e2d" or "pr/412". |
RunPage
| Field | Type | Description |
|---|---|---|
data | Run[] | |
nextCursor | string | null | Pass as cursor for the next page; null on the last page. |
RunDetail
| Field | Type | Description |
|---|---|---|
id | string | |
projectId | string | |
suiteId | string | null | |
kind | "agent" | "replay" | |
status | RunStatus | |
prompt | string | |
targetUrl | string | |
gitRef | string | null | |
workerCount | number | |
maxSteps | number | Step limit per worker. |
maxMinutes | number | Time limit per worker. |
runtime | "cloud" | "self_hosted" | |
keyMode | "managed" | "byok" | |
model | string | |
createdBy | string | Member id, "github:<login>", "schedule:<trigger>" or "api_key:<id>". |
rerunOf | string | null | |
createdAt | string (date-time) | |
startedAt | string (date-time) | null | |
finishedAt | string (date-time) | null | |
url | string | The run in the qaitai app. |
workers | Worker[] | |
issues | IssueSummary[] | |
issueCounts | object |
Worker
| Field | Type | Description |
|---|---|---|
id | string | |
label | string | |
status | RunStatus | |
startedAt | string (date-time) | null | |
finishedAt | string (date-time) | null |
IssueSummary
| Field | Type | Description |
|---|---|---|
id | string | |
severity | Severity | |
title | string | |
status | string | |
createdAt | string (date-time) | |
url | string |
Severity
"critical" | "high" | "medium" | "low"
SuitePage
| Field | Type | Description |
|---|---|---|
data | Suite[] | |
nextCursor | string | null | Pass as cursor for the next page; null on the last page. |
Suite
| Field | Type | Description |
|---|---|---|
id | string | |
projectId | string | |
name | string | |
prompt | string | |
triggers | "pr" | "deploy" | "nightly" | "weekly" | "manual"[] | |
mode | "exploratory" | "replay" | |
workerCount | number | |
enabled | boolean | |
scheduleHour | number | |
scheduleWeekday | number | |
timezone | string | |
rerunOnFailure | boolean | |
compiled | boolean | Has compiled replay steps. |
lastResult | object | null | |
createdAt | string (date-time) | |
url | string |
CreateSuiteRequest
| Field | Type | Description |
|---|---|---|
projectId | string ≤64 | a string at most 64 character(s) long |
name | string ≤80 | a string at most 80 character(s) long |
prompt | string ≤8000 | The journey to test, in plain English. |
triggers (optional) | "pr" | "deploy" | "nightly" | "weekly" | "manual"[] | Default ["manual"]. |
mode (optional) | "exploratory" | "replay" | Default exploratory. |
workerCount (optional) | integer 1–10 | a number between 1 and 10 |
scheduleHour (optional) | integer 0–23 | a number between 0 and 23 |
scheduleWeekday (optional) | integer 0–6 | a number between 0 and 6 |
timezone (optional) | string | |
rerunOnFailure (optional) | boolean | |
enabled (optional) | boolean |
RunSuiteRequest
| Field | Type | Description |
|---|---|---|
url (optional) | string ≤2048 (uri) | Run against this URL instead of the project's app URL (e.g. a preview deploy). |
gitRef (optional) | string ≤200 | a string at most 200 character(s) long |
IssuePage
| Field | Type | Description |
|---|---|---|
data | Issue[] | |
nextCursor | string | null | Pass as cursor for the next page; null on the last page. |
Issue
| Field | Type | Description |
|---|---|---|
id | string | |
projectId | string | |
runId | string | null | |
severity | Severity | |
title | string | |
description | string | |
reproSteps | string[] | |
likelyCause | object | null | |
status | string | |
externalUrl | string | null | |
createdAt | string (date-time) | |
url | string |
ProjectPage
| Field | Type | Description |
|---|---|---|
data | Project[] | |
nextCursor | string | null | Pass as cursor for the next page; null on the last page. |
Project
| Field | Type | Description |
|---|---|---|
id | string | |
name | string | |
repoFullName | string | null | |
defaultUrl | string | null | |
createdAt | string (date-time) |