Tests
Manage tests and trigger remote runs with an API key
Tests are the primary resource in SmoothDeploy. Each test holds a name, a
run configuration, and a set of steps. Tests live inside a project and a
branch. Every route in this group requires the tests:read scope in addition
to any route-level scope listed below. Requests that change data or start a run
(POST, PUT, DELETE) require an active subscription and otherwise return
403 { "message": "LICENSE_REQUIRED" }; GET requests do not. See
Authentication for headers and
Scopes for the full scope reference.
CI quick-start
The most common API use case: trigger a remote run and gate your pipeline on
the result. The API key needs the tests:read and tests:run scopes to
trigger runs, and testRuns:read to poll status and download artifacts.
# 1 — start a bulk run
RUN=$(curl -s -X POST https://api.smoothdeploy.com/v1/tests/remote-run \
-H "x-api-key: sd_your_api_key" \
-H "x-project-id: 3f8a1b2c-1111-2222-3333-444455556666" \
-H "x-branch-id: 7a9d3e4f-aaaa-bbbb-cccc-888899990000" \
-H "Content-Type: application/json" \
-d '{"testIds":["aabbccdd-1234-5678-abcd-ef0123456789"]}')
RUN_ID=$(echo "$RUN" | jq -r .data.runId)
echo "Run started: $RUN_ID"
# 2 — poll until the run reaches a terminal status
while :; do
STATUS=$(curl -s https://api.smoothdeploy.com/v1/test-runs/$RUN_ID \
-H "x-api-key: sd_your_api_key" \
-H "x-project-id: 3f8a1b2c-1111-2222-3333-444455556666" \
| jq -r .data.status)
case "$STATUS" in
completed) echo "All tests passed"; exit 0 ;;
failed) echo "Tests failed"; exit 1 ;;
cancelled) echo "Run cancelled"; exit 1 ;;
*) echo "Status: $STATUS — waiting…"; sleep 10 ;;
esac
done
# 3 — download artifacts (signed URLs, valid 60 min)
curl -s https://api.smoothdeploy.com/v1/test-runs/$RUN_ID/artifacts \
-H "x-api-key: sd_your_api_key" \
-H "x-project-id: 3f8a1b2c-1111-2222-3333-444455556666" \
| jq '.data[] | {key, label, url}'x-branch-id is optional. When omitted the project's default branch is used.
See Test runs for the full run-status API.
List tests
GET /v1/testsReturns leaf tests and directories in a project branch, with pagination.
The tests:read scope is sufficient.
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit | integer | 50 | Results per page. Minimum 1. |
page | integer | 0 | Zero-based page index. |
name | string | — | Case-insensitive substring filter on test name. |
status | string or string[] | — | Filter by status: draft, active, or archived. Repeat the parameter to match multiple values. |
tagIds | UUID or UUID[] | — | Filter to tests carrying any of these tag IDs. Repeat to pass multiple. |
type | string | all | all, ui, or api. |
testId | UUID | — | Return only direct children of this directory test. |
branch | UUID | — | Override the x-branch-id header for this request. |
sortBy | string | — | name, status, or updatedAt. |
sortOrder | string | — | asc or desc. |
When testId is set, limit is ignored and all direct children are returned
(pagination is not applied).
Response
{
"children": [
{
"id": "aabbccdd-1234-5678-abcd-ef0123456789",
"parentTestId": null,
"projectId": "proj-uuid",
"organizationId": "org-uuid",
"name": "Login flow",
"status": "active",
"directory": false,
"config": { "use": { "browserName": "chromium" }, "workers": 1 },
"vars": null,
"parameters": null,
"metadata": { "type": "ui", "description": "Login flow", "tags": [] },
"tags": [],
"childrenCount": 0,
"stepsCount": 3,
"createdAt": "2026-09-01T00:00:00.000Z",
"updatedAt": "2026-09-15T08:30:00.000Z"
}
],
"pagination": {
"page": 0,
"limit": 50,
"total": 3,
"totalPages": 1,
"hasMore": false
}
}Get a test
GET /v1/tests/:testIdReturns the test record merged with its working revision, plus its direct children (if it is a directory), its top-level steps, and its parent directory.
Response
{
"test": {
"id": "aabbccdd-1234-5678-abcd-ef0123456789",
"name": "Login flow",
"status": "active",
"directory": false,
"parentTestId": null,
"config": {
"use": { "browserName": "chromium", "baseURL": "https://app.example.com" },
"timeout": 30000,
"workers": 1
},
"updatedAt": "2026-09-15T08:30:00.000Z"
},
"children": [],
"steps": [],
"parentDirectory": null
}The branch query parameter (UUID) overrides the x-branch-id header for
this request.
Status codes
| Status | Meaning |
|---|---|
200 | Test found |
404 | Test or branch not found |
List test steps
GET /v1/tests/:testId/stepsReturns the full step tree for the test's working revision on the active
branch, including group children. tests:read is sufficient — no additional
steps scope is needed.
Query parameters
| Parameter | Type | Notes |
|---|---|---|
revisionId | UUID | Load steps from a specific snapshot revision instead of the working revision. |
Response
{
"data": {
"steps": [
{
"id": "step-uuid",
"name": "Navigate to login",
"action": { "name": "goto", "url": "https://app.example.com/login" },
"order": 0,
"group": false,
"shared": false,
"children": []
}
]
}
}Get a step
GET /v1/tests/:testId/steps/:stepIdReturns a single step from the test's working revision. :stepId is the
revision-step ID (the revisionStepId field returned in list responses). If
the step is a group, its children are included.
Response
{
"data": {
"step": {
"id": "step-uuid",
"name": "Fill username",
"action": { "name": "fill", "selector": "#username", "value": "admin" },
"group": false,
"shared": false
},
"children": []
}
}Get test file attachments
GET /v1/tests/:testId/filesReturns base64-encoded file content for any setInputFiles steps in the test.
tests:read is sufficient.
Response
{
"data": [
{
"filename": "upload.pdf",
"content": "<base64-encoded content>",
"stepId": "step-uuid"
}
]
}List revisions
GET /v1/tests/:testId/revisionsReturns all saved (snapshot) revisions for the test on the active branch, in
reverse-chronological order. tests:read is sufficient.
Response
{
"data": [
{
"id": "rev-uuid",
"message": "Added error state assertions",
"revisionType": "manual",
"createdAt": "2026-09-14T12:00:00.000Z"
}
]
}Get a revision
GET /v1/tests/:testId/revisions/:revisionIdReturns a specific revision with its full step tree. tests:read is
sufficient.
Response
{
"data": {
"revision": {
"id": "rev-uuid",
"message": "Added error state assertions",
"createdAt": "2026-09-14T12:00:00.000Z"
},
"test": { "id": "test-uuid", "name": "Login flow" },
"steps": []
}
}Create a test
POST /v1/testsRequires tests:create. Accepts multipart/form-data.
Form fields
| Field | Required | Notes |
|---|---|---|
testData | yes | JSON string. Fields described below. |
files | no | Binary file uploads for setInputFiles step actions. |
testData JSON fields
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | — | Required. Max 64 characters. |
status | string | draft | draft, active, or archived. |
directory | boolean | false | Create a directory (folder) rather than a leaf test. |
parentTestId | UUID | — | Place the new test inside this directory. |
config | object | — | Run configuration. Shape described under Run configuration. |
steps | array | — | Initial steps. Shape is complex and primarily produced by the test editor — see note below. |
tags | UUID[] | — | Tag IDs to attach. |
The steps array structure is deeply nested and generated by the console
editor. Constructing it by hand is possible but non-trivial. For programmatic
test creation, create the test shell first, then manage steps through the
console.
Response
The response is the bare test row. Name, status, and config are stored on
the revision, not the test record, so they are not present here.
{
"data": {
"id": "aabbccdd-1234-5678-abcd-ef0123456789",
"parentTestId": null,
"projectId": "proj-uuid",
"organizationId": "org-uuid",
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z"
}
}Duplicate a test
POST /v1/tests/:testId/duplicateRequires tests:create. Creates a copy of the test (or directory and all its
children) on the active branch, preserving steps, tags, and secrets.
Body (JSON, optional)
| Field | Type | Notes |
|---|---|---|
name | string | Name for the copy. Defaults to "<original name> (Copy)". Max 128 characters. |
status | string | Status for the copy: draft, active, or archived. Defaults to the original's status. |
Response
{
"data": {
"id": "new-test-uuid",
"name": "Login flow (Copy)"
}
}Update a test
PUT /v1/tests/:testIdRequires tests:update. Accepts multipart/form-data. All fields are
optional — only fields present in testData are updated.
Form fields
| Field | Required | Notes |
|---|---|---|
testData | yes | JSON string. Accepts the same fields as create: name, status, config, steps, tags, directory, parameters, vars, editVersion. |
files | no | Binary file uploads for setInputFiles step actions. |
Supplying editVersion enables optimistic-locking: if the value you send does
not match the server's current version, the request returns 409. Omit
editVersion to skip this check.
Response
200 { "data": null }Status codes
| Status | Meaning |
|---|---|
200 | Updated |
404 | Test or working revision not found |
409 | Edit conflict: another save happened since you last fetched the test |
Create a revision (snapshot)
POST /v1/tests/:testId/revisionsRequires tests:update. Saves the current working state as a named snapshot.
Returns 400 if there are no unsaved changes.
Body (JSON)
| Field | Required | Notes |
|---|---|---|
message | yes | Snapshot label. |
Response
{
"data": {
"id": "rev-uuid",
"message": "Locked for release",
"revisionType": "manual",
"createdAt": "2026-10-01T09:15:00.000Z"
}
}Bulk update status
POST /v1/tests/bulk-update-statusRequires tests:update. Sets the status of multiple tests at once.
Body (JSON)
| Field | Required | Notes |
|---|---|---|
testIds | yes | Array of test IDs. At least 1. |
status | yes | draft, active, or archived. |
Response
200 { "data": null }Bulk update tags
POST /v1/tests/bulk-update-tagsRequires tests:update. Replaces the tag set for multiple tests at once.
Body (JSON)
| Field | Required | Notes |
|---|---|---|
testIds | yes | Array of test IDs. At least 1. |
tagIds | yes | New complete tag set. Pass an empty array to clear all tags. |
Response
200 { "data": null }Delete a test
DELETE /v1/tests/:testIdRequires tests:delete. Returns 400 if the test is referenced by one or
more active schedules.
Response
200 { "data": null }Bulk delete tests
POST /v1/tests/bulk-deleteRequires tests:delete.
Body (JSON)
| Field | Required | Notes |
|---|---|---|
testIds | yes | Array of test IDs. At least 1. |
Response
200 { "data": null }Trigger a remote run (single test or directory)
POST /v1/tests/:testId/remote-runRequires tests:run. Starts a remote run for the test. If the test is a
directory, all active descendant leaf tests execute as a single run.
There is a 10-second cooldown per test between triggers. Attempting to
trigger while a run is already pending, queued, or running returns
409.
Body (JSON, optional)
| Field | Type | Notes |
|---|---|---|
config | object | Run configuration overrides. use.headless is always forced to true regardless of what you send. Shape: Run configuration. |
Response
{
"data": {
"runId": "run-uuid",
"status": "pending"
}
}For a single leaf test the totalTests field is omitted from the response;
directory and bulk runs always include it.
Status codes
| Status | Meaning |
|---|---|
200 | Run started |
400 | No active branch, or directory contains no executable tests |
404 | Test not found, or no working revision on the active branch |
409 | Test already has an active run |
429 | Cooldown period active (10 s), or concurrency limit exceeded |
Trigger a bulk remote run
POST /v1/tests/remote-runRequires tests:run. Runs an explicit list of test IDs, or every test
carrying a tag, as a single run. Directory IDs are expanded to their
descendant leaf tests. Draft and archived tests are excluded automatically.
There is a 10-second cooldown per organisation between bulk triggers.
Body (JSON)
| Field | Required | Notes |
|---|---|---|
testIds | one of | Test or directory IDs. A directory runs every active test inside it. All must belong to the same project. |
tagId | one of | Runs every active leaf test carrying this tag on the active branch. Exactly one of testIds or tagId is required. |
config | no | Run configuration overrides. use.headless is always forced to true. |
Response
{
"data": {
"runId": "run-uuid",
"status": "pending",
"totalTests": 3,
"message": "Started bulk run with 3 tests"
}
}Status codes
| Status | Meaning |
|---|---|
200 | Run started |
400 | No active branch, both or neither of testIds/tagId, or no executable tests in selection, or more than 100 active tests after directories and tags are expanded |
404 | tagId does not belong to the organization |
429 | Cooldown period active (10 s), or concurrency limit exceeded |
Export tests
POST /v1/tests/exportRequires tests:read. Returns a ZIP archive of the selected tests.
Body (JSON)
| Field | Type | Notes |
|---|---|---|
testIds | UUID[] | Specific tests to include. |
tagIds | UUID[] | Include all tests carrying any of these tags. |
directoryId | UUID | Include a directory and all its descendants. |
format | string | json (default) or pw (Playwright script). |
At least one selection field is required. The total export size is capped at 10 000 tests.
Response
Binary application/zip download. The filename is smoothdeploy-test.zip
(single test) or smoothdeploy-tests.zip (multiple).
Import tests
POST /v1/tests/importRequires tests:create. Accepts multipart/form-data. The uploaded file must
be a ZIP archive in SmoothDeploy export format.
Form fields
| Field | Required | Notes |
|---|---|---|
file | yes | The .zip archive produced by the export endpoint. |
Response
{
"data": {
"imported": {
"tests": 5,
"sharedSteps": 2,
"tags": 1,
"steps": 18
},
"errors": []
}
}errors is an array of objects with file and error fields describing any
entries in the archive that could not be imported.
Run configuration
The config object accepted by create, update, and remote-run endpoints:
| Field | Type | Default | Notes |
|---|---|---|---|
use.browserName | string | chromium | chromium, webkit, or firefox. |
use.baseURL | string | — | Base URL prepended to relative navigation targets. |
use.trace | string | retain-on-failure | off, on, retain-on-failure, or on-first-retry. |
use.video | string | off | off, on, retain-on-failure, or on-first-retry. |
use.screenshot | string | only-on-failure | off, on, or only-on-failure. |
use.actionTimeout | integer (ms) | 0 | Per-action timeout. 0 = default of 60 000. Max 3 600 000. |
use.navigationTimeout | integer (ms) | 0 | Navigation timeout. 0 = no limit. Max 3 600 000. |
retries | integer | 0 | Times to retry a failing test. |
timeout | integer (ms) | 30000 | Per-test timeout. Max 7 200 000 (2 h). |
workers | integer | 1 | Parallel workers. Limited by your plan's concurrency cap. |
repeatEach | integer | 1 | How many times to run each test. |
viewport.width | integer | 1920 | Viewport width in pixels. |
viewport.height | integer | 1080 | Viewport height in pixels. |
use.headless is accepted but ignored for remote runs — the runner always
runs headless.