SmoothDeploy Docs
API Reference

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/tests

Returns leaf tests and directories in a project branch, with pagination. The tests:read scope is sufficient.

Query parameters

ParameterTypeDefaultNotes
limitinteger50Results per page. Minimum 1.
pageinteger0Zero-based page index.
namestring—Case-insensitive substring filter on test name.
statusstring or string[]—Filter by status: draft, active, or archived. Repeat the parameter to match multiple values.
tagIdsUUID or UUID[]—Filter to tests carrying any of these tag IDs. Repeat to pass multiple.
typestringallall, ui, or api.
testIdUUID—Return only direct children of this directory test.
branchUUID—Override the x-branch-id header for this request.
sortBystring—name, status, or updatedAt.
sortOrderstring—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/:testId

Returns 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

StatusMeaning
200Test found
404Test or branch not found

List test steps

GET /v1/tests/:testId/steps

Returns 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

ParameterTypeNotes
revisionIdUUIDLoad 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/:stepId

Returns 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/files

Returns 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/revisions

Returns 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/:revisionId

Returns 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/tests

Requires tests:create. Accepts multipart/form-data.

Form fields

FieldRequiredNotes
testDatayesJSON string. Fields described below.
filesnoBinary file uploads for setInputFiles step actions.

testData JSON fields

FieldTypeDefaultNotes
namestring—Required. Max 64 characters.
statusstringdraftdraft, active, or archived.
directorybooleanfalseCreate a directory (folder) rather than a leaf test.
parentTestIdUUID—Place the new test inside this directory.
configobject—Run configuration. Shape described under Run configuration.
stepsarray—Initial steps. Shape is complex and primarily produced by the test editor — see note below.
tagsUUID[]—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/duplicate

Requires 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)

FieldTypeNotes
namestringName for the copy. Defaults to "<original name> (Copy)". Max 128 characters.
statusstringStatus 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/:testId

Requires tests:update. Accepts multipart/form-data. All fields are optional — only fields present in testData are updated.

Form fields

FieldRequiredNotes
testDatayesJSON string. Accepts the same fields as create: name, status, config, steps, tags, directory, parameters, vars, editVersion.
filesnoBinary 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

StatusMeaning
200Updated
404Test or working revision not found
409Edit conflict: another save happened since you last fetched the test

Create a revision (snapshot)

POST /v1/tests/:testId/revisions

Requires tests:update. Saves the current working state as a named snapshot. Returns 400 if there are no unsaved changes.

Body (JSON)

FieldRequiredNotes
messageyesSnapshot 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-status

Requires tests:update. Sets the status of multiple tests at once.

Body (JSON)

FieldRequiredNotes
testIdsyesArray of test IDs. At least 1.
statusyesdraft, active, or archived.

Response

200 { "data": null }

Bulk update tags

POST /v1/tests/bulk-update-tags

Requires tests:update. Replaces the tag set for multiple tests at once.

Body (JSON)

FieldRequiredNotes
testIdsyesArray of test IDs. At least 1.
tagIdsyesNew complete tag set. Pass an empty array to clear all tags.

Response

200 { "data": null }

Delete a test

DELETE /v1/tests/:testId

Requires 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-delete

Requires tests:delete.

Body (JSON)

FieldRequiredNotes
testIdsyesArray of test IDs. At least 1.

Response

200 { "data": null }

Trigger a remote run (single test or directory)

POST /v1/tests/:testId/remote-run

Requires 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)

FieldTypeNotes
configobjectRun 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

StatusMeaning
200Run started
400No active branch, or directory contains no executable tests
404Test not found, or no working revision on the active branch
409Test already has an active run
429Cooldown period active (10 s), or concurrency limit exceeded

Trigger a bulk remote run

POST /v1/tests/remote-run

Requires 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)

FieldRequiredNotes
testIdsone ofTest or directory IDs. A directory runs every active test inside it. All must belong to the same project.
tagIdone ofRuns every active leaf test carrying this tag on the active branch. Exactly one of testIds or tagId is required.
confignoRun 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

StatusMeaning
200Run started
400No 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
404tagId does not belong to the organization
429Cooldown period active (10 s), or concurrency limit exceeded

Export tests

POST /v1/tests/export

Requires tests:read. Returns a ZIP archive of the selected tests.

Body (JSON)

FieldTypeNotes
testIdsUUID[]Specific tests to include.
tagIdsUUID[]Include all tests carrying any of these tags.
directoryIdUUIDInclude a directory and all its descendants.
formatstringjson (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/import

Requires tests:create. Accepts multipart/form-data. The uploaded file must be a ZIP archive in SmoothDeploy export format.

Form fields

FieldRequiredNotes
fileyesThe .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:

FieldTypeDefaultNotes
use.browserNamestringchromiumchromium, webkit, or firefox.
use.baseURLstring—Base URL prepended to relative navigation targets.
use.tracestringretain-on-failureoff, on, retain-on-failure, or on-first-retry.
use.videostringoffoff, on, retain-on-failure, or on-first-retry.
use.screenshotstringonly-on-failureoff, on, or only-on-failure.
use.actionTimeoutinteger (ms)0Per-action timeout. 0 = default of 60 000. Max 3 600 000.
use.navigationTimeoutinteger (ms)0Navigation timeout. 0 = no limit. Max 3 600 000.
retriesinteger0Times to retry a failing test.
timeoutinteger (ms)30000Per-test timeout. Max 7 200 000 (2 h).
workersinteger1Parallel workers. Limited by your plan's concurrency cap.
repeatEachinteger1How many times to run each test.
viewport.widthinteger1920Viewport width in pixels.
viewport.heightinteger1080Viewport height in pixels.

use.headless is accepted but ignored for remote runs — the runner always runs headless.