API overview
Everything the Plainwright app does goes through a JSON REST API, so you can script it: create tests, trigger runs, wait for results, quarantine flaky tests or file issues. This page covers the basics. The REST API reference lists every endpoint with its parameters and response schema, generated from the backend’s OpenAPI schema.
Base URL
Section titled “Base URL”With the default Docker Compose setup, the API is served through nginx under /api:
http://localhost:8080/apiOn a deployed server use your public origin, such as https://plainwright.example.com/api. The
examples on this page read it from PLAINWRIGHT_URL (without /api).
Authenticate
Section titled “Authenticate”The API uses bearer tokens (JWT). Sign in once, keep the token, and send it on every request.
- Get a token from
POST /api/auth/login. This one endpoint takes a form body (application/x-www-form-urlencoded) withusernameset to the account’s email, not JSON. - Send it as
Authorization: Bearer <access_token>on every other request. - Get a new token when it expires (after 7 days by default; the server sets this with
ACCESS_TOKEN_EXPIRE_MINUTES) or is revoked. Expired, revoked or invalid tokens get401.
export PLAINWRIGHT_URL=http://localhost:8080export PLAINWRIGHT_TOKEN=$(curl -fsS -X POST "$PLAINWRIGHT_URL/api/auth/login" \ --data-urlencode "username=ci-bot@acme.example" \ --data-urlencode "password=$PLAINWRIGHT_PASSWORD" | jq -r .access_token)
curl -fsS "$PLAINWRIGHT_URL/api/auth/me" -H "Authorization: Bearer $PLAINWRIGHT_TOKEN"$env:PLAINWRIGHT_URL = 'http://localhost:8080'$login = Invoke-RestMethod -Method Post -Uri "$env:PLAINWRIGHT_URL/api/auth/login" ` -Body @{ username = 'ci-bot@acme.example'; password = $env:PLAINWRIGHT_PASSWORD }$env:PLAINWRIGHT_TOKEN = $login.access_token
Invoke-RestMethod -Uri "$env:PLAINWRIGHT_URL/api/auth/me" ` -Headers @{ Authorization = "Bearer $env:PLAINWRIGHT_TOKEN" }{ "access_token": "eyJhbGciOiJIUzI1NiIs…", "token_type": "bearer", "user": { "id": "…", "email": "ci-bot@acme.example", "name": "CI bot", "avatar_url": null, "email_verified": true, "onboarding_completed_at": null }}POST /api/auth/register (JSON {email, password, name}) creates an account and returns the same
shape. The password must be 8 or more characters, at most 72 bytes, and not the email address.
Google and GitHub sign-in are browser flows for the app: the callback hands the browser a one-time
code, which the app trades for a token with POST /api/oauth/exchange (it only works in the browser
that started the sign-in); see Sign-in and email.
Sessions and revocation
Section titled “Sessions and revocation”Every token carries the account’s token version. These calls bump it, which revokes every token issued before:
| Call | Returns |
|---|---|
POST /api/auth/change-password ({current_password, new_password}) |
{message, access_token}: a fresh token for the caller; every other session is signed out |
POST /api/auth/logout-all |
{message, access_token}: the same, without changing the password |
| A completed password reset | The account’s other sessions are signed out |
| Removal from the last project the account belonged to | Every session of that account is signed out |
Keep the access_token these calls return, or the next request with the old token gets 401.
Rate limits
Section titled “Rate limits”Failed sign-ins, registrations and password-reset requests are rate-limited. A blocked request gets
429 with “Too many attempts — wait a few minutes and try again.” and a Retry-After header.
Scripts that sign in once and reuse the token never hit these limits. The exact limits are in the
security model.
Three ways to authenticate
Section titled “Three ways to authenticate”| Credential | Header | Works for |
|---|---|---|
| User token | Authorization: Bearer <jwt> |
Every endpoint, limited by the user’s role on each project |
| Project webhook secret | X-Webhook-Secret: <secret> |
Only POST /api/runs/webhook/{project_id} and GET /api/batches/{batch_id} (see CI webhook and gates) |
| None | none | Health (/health), login, register, password reset, OAuth providers, the Stripe webhook (signature-checked), and screenshot files |
Roles and access
Section titled “Roles and access”Each project gives a user one role: viewer, editor, admin, or owner (who created it). Endpoints check the role on the project the resource belongs to:
404means you can’t see the project at all, so Plainwright doesn’t reveal that it exists.403with"Requires editor access"(or admin) means you can see it but your role is too low.
The full table of who can do what is in Team and roles.
Conventions
Section titled “Conventions”| Topic | Rule |
|---|---|
| Bodies | JSON (Content-Type: application/json), except login (form) and file uploads (multipart/form-data) |
| IDs | UUID strings. A malformed ID returns 404 Not found, not 500. Batch IDs are 32 hex characters. |
| Errors | {"detail": "message"}. Validation errors (422) carry FastAPI’s list of field errors in detail. |
| Times | ISO 8601 strings in UTC. Some older fields use a space instead of T. |
| Status | Runs have status (pending, running, passed, failed, error) and effective_status, which applies a human override. Use effective_status for decisions. |
| Paging | GET /api/runs takes limit (up to 200) and offset and returns {total, limit, offset, runs}. GET /api/runs/test-case/{id} takes limit (1–100) and offset. |
| Queue down | Endpoints that queue runs return 503 when Redis is unreachable. Retry later. |
| Secrets | Values are never returned in full. Provider keys come back masked to their first 6 and last 4 characters; vault secrets and integration tokens as •••• plus at most the last 4; passwords fully masked. |
Common tasks
Section titled “Common tasks”H="Authorization: Bearer $PLAINWRIGHT_TOKEN"
# Projects you can access (each includes my_role)curl -fsS "$PLAINWRIGHT_URL/api/projects/" -H "$H"
# Modules (board columns) and their testscurl -fsS "$PLAINWRIGHT_URL/api/modules/project/$PROJECT_ID" -H "$H"
# Create a test (editor)curl -fsS -X POST "$PLAINWRIGHT_URL/api/test-cases/" -H "$H" -H "Content-Type: application/json" \ -d '{"module_id": "'"$MODULE_ID"'", "name": "Guest checkout", "natural_language": "Add any product to the cart, check out as a guest and pay with the test card. Check that an order number is shown.", "kind": "ui", "retries": 1}'
# Run it against an environment in Firefox (editor)RUN_ID=$(curl -fsS -X POST "$PLAINWRIGHT_URL/api/runs/trigger/$TEST_ID?environment_id=$ENV_ID&browser=firefox" \ -H "$H" | jq -r .id)
# Poll the run until it finisheswhile :; do STATUS=$(curl -fsS "$PLAINWRIGHT_URL/api/runs/$RUN_ID" -H "$H" | jq -r .effective_status) case "$STATUS" in pending|running) sleep 5 ;; *) echo "$STATUS"; break ;; esacdone$h = @{ Authorization = "Bearer $env:PLAINWRIGHT_TOKEN" }$api = "$env:PLAINWRIGHT_URL/api"
Invoke-RestMethod -Uri "$api/projects/" -Headers $hInvoke-RestMethod -Uri "$api/modules/project/$env:PROJECT_ID" -Headers $h
$test = @{ module_id = $env:MODULE_ID; name = 'Guest checkout'; kind = 'ui'; retries = 1 natural_language = 'Add any product to the cart, check out as a guest and pay with the test card. Check that an order number is shown.' }Invoke-RestMethod -Method Post -Uri "$api/test-cases/" -Headers $h -ContentType 'application/json' -Body ($test | ConvertTo-Json)
$run = Invoke-RestMethod -Method Post -Uri "$api/runs/trigger/$($env:TEST_ID)?browser=firefox" -Headers $hdo { Start-Sleep 5; $run = Invoke-RestMethod -Uri "$api/runs/$($run.id)" -Headers $h }while ($run.effective_status -in 'pending', 'running')$run.effective_statusPOST /api/runs/trigger/{test_case_id} returns the new run straight away with status pending.
Its optional query parameters are environment_id, browser (chromium, firefox, webkit)
and device (desktop, iphone, pixel, ipad).
Follow a run live
Section titled “Follow a run live”GET /api/runs/{run_id}/stream is a Server-Sent Events
stream of the run’s live events: start, step, frame (screenshots), evidence, log, turn
(conversation turns), eval_row, trial and finally done. It needs the bearer header, so use
fetch or curl -N rather than the browser’s EventSource, which can’t send headers.
curl -N "$PLAINWRIGHT_URL/api/runs/$RUN_ID/stream" -H "Authorization: Bearer $PLAINWRIGHT_TOKEN"Events are not stored: you see them from the moment you connect. Read the finished run with
GET /api/runs/{run_id}.
Endpoint map
Section titled “Endpoint map”The generated reference groups endpoints by these tags.
| Area | Prefix | Covers |
|---|---|---|
| Auth | /api/auth, /api/auth/password, /api/oauth |
Login, register, profile, password change and reset, sign out everywhere, email confirmation, Google/GitHub sign-in and the one-time code exchange |
| Projects | /api/projects |
Projects, members, project files, webhook secret rotation, test notification |
| Modules | /api/modules |
Board columns; run a module or the whole project |
| Test Cases | /api/test-cases |
Create, read, update and delete tests |
| Templates, Onboarding | /api/templates, /api/onboarding |
Starter tests and the guided setup |
| Runs | /api/runs, /api/run-actions, /api/batches |
Trigger, list and stream runs; overrides, root cause, chat; batch status and CI gate |
| Reliability, Triage | /api/reliability, /api/triage |
Flaky tests, quarantine, rerun failed, alert status; failure clusters and issue filing |
| Visual | /api/visual |
Baselines and approvals |
| Evals | /api/datasets |
Datasets, eval summaries and per-row results |
| Secrets | /api/secrets, /api/auth-states |
The secret vault and saved logins |
| Integrations | /api/integrations, /api/connectors |
Jira, Linear, GitHub, Figma, Notion, IMAP; drafting tests from them |
| AI Configs | /api/ai-configs |
AI providers, connection tests, the supported model list (/models), Ollama models |
| Analytics | /api/analytics |
Overview, trends, flakiness, usage, score trends |
| Environments | /api/environments |
Per-project base URLs |
| AI Crawler | /api/crawler |
Suggest tests from a URL, PDF or OpenAPI spec; follow and cancel a crawl |
| Billing, Files | /api/billing, /api/files |
Plans and usage (when enabled); stored screenshots and evidence |
| System | /api/system |
Configuration warnings for project owners and admins, such as a weak SECRET_KEY |
/metrics (Prometheus) is internal and not part of the reference; see
Monitoring.
Keep the reference current
Section titled “Keep the reference current”The reference pages are generated from docs-site/openapi/plainwright.json. After changing backend
endpoints, regenerate it from the docs site:
cd docs-sitenpm run openapiThis imports the FastAPI app without starting it (no database, Redis or Docker needed) and writes the schema. Commit the updated JSON with your change; see Contributing.