# Kintsugi QA API > Kintsugi QA is a browser-based UI test automation platform. This file is the > complete capability spec of the Kintsugi QA public HTTP API for autonomous > AI agents. Given this file plus an API key (`X-API-Key: ksk_<64 hex chars>`), > an agent can — with no other documentation — discover the user's > organization and projects, write new test cases as JSON (single-actor > browser scenarios), run them, poll for results, and download videos, > screenshots, traces, network logs, and console logs. Every endpoint, > request/response field, step type, assertion type, status enum, and > variable placeholder usable via API key is documented below. Endpoints not > usable with API keys are listed explicitly so they are not attempted. ## 1. Authentication & conventions - **Header**: send `X-API-Key: ksk_<64 hex characters>` on every request. A query parameter `?api_key=ksk_...` also works, but the header is preferred (it won't leak into logs/URLs). - **Base URL**: `https://kintsugi-qa.com/api/v1`. - **Content-Type**: `application/json` for all JSON request bodies, except `POST /files` which is `multipart/form-data`. - **Org scoping**: an API key belongs to exactly one organization and grants read/write access to every project, test case, suite, run, artifact, secret, file, schedule, and webhook token within that organization. `GET /orgs` returns only the key's own org. `GET /orgs/{id}` works for that org (any other org ID is rejected by the authorization check). - **Pagination**: list endpoints that paginate return `{"items": [...], "count": N, "next_cursor": "...", "has_more": bool}`. Pass `?cursor=` to fetch the next page and `?limit=` to control page size. Some simple list endpoints (projects, secrets, files, schedules, webhook-tokens, orgs) return `{"items": [...], "count": N}` without cursor pagination (all results in one page). ### Error response shapes Most errors: ```json { "error": "human-readable message", "request_id": "abc-123" } ``` (`request_id` may be empty/omitted; `code` may also appear for some domain-level errors, e.g. `"code": "NOT_FOUND"`.) Structural scenario validation failures use a slightly different shape. Step ids must be unique within each actor for EVERY test case (single- and multi-actor — duplicates corrupt run results and self-healing patches); collaborative scenarios additionally get full barrier validation (see section 4.7): ```json { "error": "barrier \"key-ready\": must have at least 2 participating actors, got 1; ...", "code": "INVALID_SCENARIO" } ``` `error` is a `; `-joined list of every problem found, so a single response can report multiple issues at once. ### Common status codes - `200 OK` — successful read/update. - `201 Created` — resource created (test case, project, secret, file, schedule, webhook token, suite). - `202 Accepted` — async operation started; response body is the created run object (or, for webhook tokens with a body, also a run object). - `204 No Content` — successful delete. - `400 Bad Request` — invalid JSON, missing required fields, or (for API keys) a collaborative (multi-actor) scenario that fails structural validation on a test-case create/update endpoint (`code: "INVALID_SCENARIO"`, see section 4.7). - `401 Unauthorized` — missing/invalid API key. - `403 Forbidden` — endpoint not available with API-key authentication (see section "Endpoints NOT available with API keys" below), or the key's org lacks access to the requested project/org. Body: `{"error": "this endpoint is not available with API key authentication", "code": "FORBIDDEN"}` for blocked endpoints, or `{"error": "access denied"}` for authorization failures. - `404 Not Found` — resource does not exist or is not visible to this org. - `409 Conflict` — duplicate slug/name on create. - `413 Request Entity Too Large` — file upload exceeds 20 MB. - `500 Internal Server Error` — unexpected server error. ### Endpoints NOT available with API keys (do not call these) - `POST /test-cases/generate`, `POST /test-cases/{id}/regenerate` (AI test generation) - All `/probes` and `/authoring-sessions` routes (interactive browser-driven authoring) - `POST /suites/{id}/probe` (AI-generated suite transitions) - Organization mutations: `POST/PUT/DELETE /orgs`, all `/orgs/{id}/members*` and `/orgs/{id}/invites` routes - API key management: `POST/GET/DELETE /orgs/{id}/api-keys` - `/admin/*`, `/dev/login`, `/auth/*` All requests to these return `403 {"error":"this endpoint is not available with API key authentication","code":"FORBIDDEN"}` (or `401`/redirect for the auth/admin routes). **An agent writes test JSON itself — that is the entire point of this API.** --- ## 2. RECOMMENDED AGENT WORKFLOW Step-by-step, with `curl` examples. Replace `` and IDs with real values. Full request/response shapes for every endpoint used here are in section 7. ### 2.1 Discover the organization ```bash curl https://kintsugi-qa.com/api/v1/orgs -H "X-API-Key: " # => {"items":[{"id":"5b3c2a1d-...","name":"Acme Inc","slug":"acme", ...}], "count":1} ``` Save `items[0].id` as ``. ### 2.2 List projects (create one if none exist) ```bash curl "https://kintsugi-qa.com/api/v1/projects?org_id=" -H "X-API-Key: " ``` Save `items[0].id` as ``. If `items` is empty, create a project: ```bash curl -X POST https://kintsugi-qa.com/api/v1/projects \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{ "org_id": "", "slug": "marketing-site", "name": "Marketing Site", "description": "Public marketing site", "base_url": "https://staging.example.com", "environments": [ { "name": "staging", "base_url": "https://staging.example.com" }, { "name": "production", "base_url": "https://example.com" } ] }' ``` Required: `slug`, `name`, `org_id`. Optional: `description`, `base_url`, `environments` (array of `{name, base_url}`), `custom_headers` (map of header name -> value, sent on every request the test runner makes). Response (`201 Created`) is the full project object — see 7.2. ### 2.3 Create a test case `POST /test-cases` — see section 4 for the full scenario/step JSON schema. Minimal example: ```bash curl -X POST https://kintsugi-qa.com/api/v1/test-cases \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "User can log in", "description": "Logs in with valid credentials and lands on the dashboard", "default_base_url": "https://staging.example.com", "scenario": { "actors": [ { "id": "user", "name": "User", "browser": "chromium", "viewport": { "width": 1280, "height": 800 }, "steps": [ { "id": "step-1", "type": "navigate", "url": "{{BASE_URL}}/login", "description": "Go to login" }, { "id": "step-2", "type": "fill", "selector": "#email", "value": "demo@example.com", "description": "Enter email" }, { "id": "step-3", "type": "fill", "selector": "#password", "value": "{{SECRET_DEMO_PASSWORD}}", "description": "Enter password" }, { "id": "step-4", "type": "click", "selector": "button[type=submit]", "description": "Submit login form" }, { "id": "step-5", "type": "assert", "selector": "[data-testid=dashboard-heading]", "assertion": { "type": "visible" }, "description": "Dashboard heading visible" } ] } ] } }' ``` Response (`201 Created`) is the TestCase object (section 7.3); save `id` as ``. The example above has a single actor, which is the default and recommended shape for most tests. `scenario.actors` MAY also contain **2 or more actors** with `barriers` for collaborative (multi-actor) tests — see section 4.7 for the schema, validation rules, and a full worked example. ### 2.4 Trigger a run ```bash curl -X POST https://kintsugi-qa.com/api/v1/runs \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{ "test_case_id": "", "project_id": "" }' ``` Fields: `test_case_id` (required), `project_id` (required), `test_case_version` (optional int — pin to a version, defaults to latest), `environment` (optional — **omit it unless you need to override the base URL**; if provided it must be either a full URL like `https://staging.example.com` or the name of an environment defined in the project's `environments[]`, which is resolved to its `base_url` at trigger time; an unknown name is rejected with 400. When omitted, `{{BASE_URL}}` resolves to the test case's `default_base_url`). Response (`202 Accepted`) is a TestRun object (section 6.2) with `status: "pending"`; save `id` as ``. ### 2.5 Poll for completion Poll every **3-5 seconds**: ```bash curl https://kintsugi-qa.com/api/v1/runs//status -H "X-API-Key: " # => {"id":"...", "status":"passed", "started_at":"...", "completed_at":"...", "duration_ms":42000} ``` `completed_at`/`duration_ms` are `null` until the run finishes. Stop polling once `status` is one of the **terminal** values: `passed`, `failed`, `cancelled`, `timed_out` (`pending`, `running`, `healing` are non-terminal — see section 6.1). For full details (per-step results, errors, artifact keys), use `GET /runs/` (section 6.2). ### 2.6 List and download artifacts ```bash curl https://kintsugi-qa.com/api/v1/runs//artifacts -H "X-API-Key: " # => {"run_id":"...", "artifacts": ["runs///video-.webm", "runs///video-.vtt", ...]} ``` `artifacts` is a flat array of S3 key strings. Key names include generated timestamps — **never construct artifact keys yourself; always copy the exact key strings from this response.** Download any key two ways: **A — presigned URL** (no auth header needed on the download itself, valid 15 minutes): ```bash curl "https://kintsugi-qa.com/api/v1/artifacts/presign/" -H "X-API-Key: " # => {"url": "https://kintsugi-prod-artifacts-529589820112.s3.us-east-1.amazonaws.com/...?X-Amz-...", "key": ""} curl -o video.webm "https://kintsugi-prod-artifacts-529589820112.s3.us-east-1.amazonaws.com/...&X-Amz-..." ``` **B — authenticated stream** (supports HTTP `Range` for video seeking): ```bash curl "https://kintsugi-qa.com/api/v1/artifacts/" -H "X-API-Key: " -o video.webm ``` --- ## 3. SUITES A suite is an ordered list of test cases executed sequentially **in a single browser session** (one browser context, not restarted between test cases). Run-scoped variables (from `extract` steps) carry over from one test case to the next within the suite run. ### Create — `POST /suites` ```json { "project_id": "", "name": "Core checkout flow", "description": "Login, then place an order", "steps": [ { "test_case_id": "" }, { "test_case_id": "", "transition": "User is already logged in; navigate to /products" } ] } ``` - `project_id` (required), `steps` (required array of `{test_case_id, transition?}`). - `name` is optional — auto-generated from the referenced test case names if omitted. - `transition` (optional string) describes how to bridge from the previous test case's end state to this one's start state. It is informational only via the API — `transition_steps` are not auto-generated for API-created suites (that requires `POST /suites/{id}/probe`, not available with API keys), but you may hand-author `transition_steps` via `PUT /suites/{id}`. Response (`201 Created`) is the full Suite object (section 7.5). ### Run — `POST /suites/{id}/run` ```bash curl -X POST https://kintsugi-qa.com/api/v1/suites//run \ -H "X-API-Key: " -H "Content-Type: application/json" \ -d '{}' ``` Body is optional (`environment` only — same semantics as on `POST /runs`: omit it, or pass a full URL / a project environment name). Response (`202 Accepted`) is a TestRun object identical in shape to a single-test-case run, but with `suite_id` set and `actor_runs`/`suite_snapshot` reflecting all test cases in the suite. Poll exactly as in 2.5. Suite runs also appear in `GET /runs?project_id=...&suite_id=`. --- ## 4. TEST DEFINITION SCHEMA ### 4.1 Test case create/update body `POST /test-cases` and `PUT /test-cases/{id}`: ```json { "project_id": "", // required (POST only; PUT resolves from URL) "name": "User can log in", // required "description": "optional text", // optional "scenario": { "actors": [ ... ] }, // required — see 4.2 "default_base_url": "https://staging.example.com", // strongly recommended when steps use {{BASE_URL}} "depends_on": "", // optional, PUT only — prerequisite test case "transition": "description string" // optional, PUT only — how to bridge from depends_on's end state } ``` `default_base_url` is what `{{BASE_URL}}` resolves to when no `environment` override is passed at run time. **Always set it if any step URL uses `{{BASE_URL}}`** — otherwise the run fails at setup. Alternatively, use absolute URLs in `navigate` steps and skip both. `PUT /test-cases/{id}` creates a new version of the test case (response `version` increments). `id` in the URL may be a UUID or a slug; if it's a slug, pass `?project_id=` as a query parameter. #### `depends_on` and `transition` (dependency chains) - `depends_on` (settable via **`PUT` only** — `POST /test-cases` does not accept it; create the test case first, then `PUT` to set `depends_on`) is the ID of a prerequisite test case. `transition` is a free-text description of how to bridge from that prerequisite's end state to this test case's start state (e.g. `"User is already logged in; navigate to /settings"`). `transition` is informational/documentation only — it is not turned into executable steps automatically. - **Standalone run** (`POST /runs` with this test case, no suite): the orchestrator walks the `depends_on` chain (grandparent → parent → this test case) and **prepends every ancestor's steps**, in execution order, to actor 0's steps for that run — all in **one browser session**. A `transition` on this test case (or on any ancestor) becomes an executable `navigate` step ONLY if its text contains an http(s) URL (the first URL is extracted and navigated to); otherwise the transition is purely informational and no step is inserted. A cycle in the `depends_on` chain is detected and silently broken (the chain stops at the repeated test case) rather than looping forever or erroring. - **Suite run**: dependency steps are **NOT** prepended. Instead, `POST /suites` and `PUT /suites/{id}` validate that every test case's full `depends_on` chain (including transitive ancestors) appears **earlier** in the suite's `steps` array — if not, the suite create/update is rejected with an error naming the missing prerequisite. Each test case may appear in a suite at most once for the purposes of this check. ### 4.2 Scenario ```json { "actors": [ { "id": "user", "name": "User", "description": "optional", "browser": "chromium", "viewport": { "width": 1280, "height": 800, "mobile": false }, "steps": [ /* ordered Step[] — see 4.4 */ ] } ] } ``` The example above shows the default single-actor shape. `actors` MAY instead contain **2 or more actors**, each with its own `steps`, plus a top-level `barriers` array for synchronization between them — see **section 4.7 (Multi-actor / collaborative tests)** for the full schema, validation rules, and a worked example. Everything in this section and 4.3-4.6 applies equally to each actor in a collaborative scenario. | field | type | required | notes | |---|---|---|---| | `id` | string | yes | stable identifier for the actor | | `name` | string | yes | human-readable name | | `description` | string | no | optional notes | | `browser` | `"chromium"` \| `"firefox"` \| `"webkit"` | no | defaults to `chromium` | | `viewport` | `{width: int, height: int, mobile?: bool}` | no | pixel dimensions; `mobile: true` enables touch emulation | | `steps` | Step[] | yes | ordered list of actions, executed top to bottom | ### 4.3 Common step fields Every step is an object with at least `id` and `type`. These fields apply across step types (only the relevant ones are used per type — see 4.4): | field | type | applies to | notes | |---|---|---|---| | `id` | string | all | required, unique within the actor's steps. Used in run results and self-healing patches. | | `type` | string | all | required. One of: `navigate`, `click`, `fill`, `select`, `hover`, `scroll`, `wait`, `assert`, `screenshot`, `upload`, `download`, `extract`, `press`, `drag`, `barrier_wait`, `barrier_open` (the last two are for collaborative/multi-actor scenarios — see section 4.7). | | `selector` | string | click, fill, select, hover, scroll, assert, upload, extract, wait, press (optional), drag | CSS selector for the target element. Prefer stable selectors: `#id`, `[data-testid=...]`, `[name=...]`, `[aria-label=...]` over brittle nth-child/class chains. | | `alternative_selectors` | string[] | click, fill, select, hover, scroll, assert, upload, drag, extract | optional fallback selectors tried by self-healing if the primary selector fails (see Self-healing, section 6). | | `value` | string | fill, select, scroll, wait, screenshot, upload, press | meaning depends on step type — see 4.4. For `press`, the key name (e.g. `"Escape"`, `"Control+A"`). Supports `{{...}}` variable placeholders (section 4.5). | | `url` | string | navigate | destination URL. Supports `{{BASE_URL}}` and other placeholders. | | `assertion` | object | assert | `{type, expected?}` — see 4.4 `assert`. | | `position` | `{x, y}` | click, drag | optional pixel offset from the target element's top-left corner. Defaults to the element's center. For `drag`, this is the offset within the **source** element. | | `target_selector` | string | drag | destination element selector. Defaults to the source `selector` (same-element drag). | | `target_position` | `{x, y}` | drag | optional pixel offset within the target element. Defaults to the element's center. | | `timeout` | int (ms) | all (esp. navigate, wait, assert, extract) | per-step timeout in milliseconds. **Clamped server-side to 5000-120000 ms**; for `navigate` steps the effective minimum is **30000 ms** (a smaller value is raised to 30000). Values <= 0 or omitted default to 5000ms (or the type's own default — see per-step notes). Always specify timeouts explicitly in **milliseconds** (5000 = 5 seconds); never emit values like `30` meaning "30 seconds" — that becomes 30ms and is clamped up to 5000ms, likely causing the step to fail too fast. | | `description` | string | all | human-readable description shown in run results/UI. Strongly recommended on every step. | | `optional` | bool | all | if `true`, a failure on this step is tolerated and the run continues to the next step (e.g. for cookie-consent banners or one-time modals that may or may not appear). | | `variable` | string | extract | name of the run-scoped variable to store the extracted value in. | | `attribute` | string | extract, assert (with `assertion.type: "attribute"`) | which HTML attribute to read/compare. Empty/omitted means innerText (extract) or is required (assert attribute). | | `regex` | string | extract | regex with exactly one capture group; if set, the **first capture group** of the match against the extracted raw text is stored instead of the raw text. | | `barrier_id` | string | `barrier_wait`, `barrier_open` | required for these step types — references a `Barrier.id` from `scenario.barriers`. Single-actor scenarios omit this entirely. See section 4.7. | ### 4.4 Step types (full reference, one example each) #### navigate Navigates the page to a URL. - `url` (string, required) — supports `{{BASE_URL}}` and other placeholders. - Effective minimum timeout is 30000ms regardless of `timeout` value. ```json { "id": "step-1", "type": "navigate", "url": "{{BASE_URL}}/login", "description": "Go to the login page" } ``` #### click Clicks the element matched by `selector`. Includes built-in handling for custom dropdowns/comboboxes. - `selector` (string, required) - `alternative_selectors` (string[], optional) - `position` (`{x, y}`, optional) — click at this pixel offset from the element's top-left corner instead of its center. ```json { "id": "step-4", "type": "click", "selector": "button[type=submit]", "alternative_selectors": ["#login-submit"], "description": "Submit the login form" } ``` #### fill Types `value` into the input/textarea matched by `selector` (clears existing content first). - `selector` (string, required) - `value` (string, required) — supports `{{SECRET_*}}`, `{{VAR:*}}`, `{{BASE_URL}}`, `{{FILE_*}}` placeholders. If the resolved value came from a `{{SECRET_*}}` placeholder it is treated as masked: the runner refuses to type it into anything but a `` and never logs it in plaintext (recorded as `"***"` in step results). - `alternative_selectors` (string[], optional) ```json { "id": "step-3", "type": "fill", "selector": "#password", "value": "{{SECRET_DEMO_PASSWORD}}", "description": "Enter password" } ``` #### select Chooses an option in a ``/`