Import Test Plans API
Programmatically create and list test plans, labels, and credentials. Built for one-shot import of existing test suites, drivable by the testlab CLI or an AI agent.
Import Test Plans API
These endpoints let you create test plans (plus the labels, credentials, and pipeline pre-steps they depend on) without the dashboard. They are the foundation of the testlab CLI and are designed for a one-shot import: point an AI agent at your existing tests, have it convert each into a plan, and push them into your account.
POST https://test-lab.ai/api/v1/test-plans
GET https://test-lab.ai/api/v1/test-plans
POST https://test-lab.ai/api/v1/labels
GET https://test-lab.ai/api/v1/labels
POST https://test-lab.ai/api/v1/credentials
GET https://test-lab.ai/api/v1/credentialsAuthentication
Every request uses a full-scope API key:
Authorization: Bearer tl_xxxxxCreate a key under Settings → API Keys (/admin/settings/api-keys). The key resolves to exactly one account, so the key you use IS the import target. To import into an organization account, mint the key while you have that org selected.
The key's account is the only account these endpoints touch. Labels, pre-steps, and credentials are always resolved within that account, never another.
Create a test plan
POST /api/v1/test-plansImported plans are account-scoped (no project), so the target URL lives in the prompt text. That is exactly what the test-lab plan format already produces.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Plan name (max 200 chars) |
prompt | string | Yes | The natural-language test, with explicit URL(s) and {{credentials.X}} placeholders (max 32 KB) |
testType | string | No | "quickTest" or "deepTest" |
agentType | string | No | functional (default), accessibility, uiux, exploratory, performance, or security |
devices | string[] | No | e.g. ["Desktop Chrome"] (default) or ["iPhone 15 Pro"] |
labels | (string | number)[] | No | Label names (auto-created) and/or existing label ids. Max 25. |
preSteps | object[] | No | Pipeline pre-steps. Each is { testPlanId } or { name } (+ optional inputValues). Max 25. |
failOnPreStepFailure | boolean | No | Default true |
cookies | array | No | [{ name, value, domain }] injected at run time |
headers | array | No | [{ name, value }] injected at run time |
projectId and proxyCountry (geolocation) are not configurable via import yet. Imported plans have no project; set those in the dashboard if needed.
curl -X POST https://test-lab.ai/api/v1/test-plans \
-H "Authorization: Bearer tl_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Login smoke test",
"prompt": "Go to https://app.example.com/login and sign in with {{credentials.email}} / {{credentials.password}}. Confirm the dashboard loads.",
"testType": "quickTest",
"agentType": "functional",
"labels": ["smoke", "auth"]
}'Response (201):
{
"testPlan": {
"id": 123,
"name": "Login smoke test",
"prompt": "Go to https://app.example.com/login …",
"default_agent_type": "functional",
"labels": [{ "id": 5, "name": "smoke", "color": "#22c55e" }],
"preSteps": []
}
}Pipeline pre-steps
To make one plan depend on another, reference the dependency by name (resolved to the most recent plan with that name in your account) or by id:
curl -X POST https://test-lab.ai/api/v1/test-plans \
-H "Authorization: Bearer tl_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Checkout",
"prompt": "From the cart at https://app.example.com/cart, complete checkout.",
"preSteps": [{ "name": "Login smoke test" }]
}'Create the dependency first so the name resolves. An unresolved pre-step returns 404 (the create API fails loudly rather than dropping a dependency silently). The testlab import command handles ordering for you.
List test plans
GET /api/v1/test-plans?page=1&limit=25Returns a lean, paginated view, enough to de-duplicate before importing and to look up ids/names for pre-step wiring:
{
"testPlans": [
{
"id": 123,
"name": "Login smoke test",
"default_agent_type": "functional",
"labels": [{ "id": 5, "name": "smoke" }],
"preSteps": []
}
],
"total": 1,
"page": 1,
"limit": 25,
"totalPages": 1
}Labels
POST /api/v1/labels { "name": "smoke" }
GET /api/v1/labelsPOST is idempotent: a name that already exists returns the existing label (no error), so re-running an import is safe. You rarely need to call this directly, since POST /api/v1/test-plans auto-creates labels passed by name.
Credentials
POST /api/v1/credentials { "key": "email", "value": "qa@example.com" }
GET /api/v1/credentialsPOST is an upsert (create, or replace the value of an existing key). Values are stored AES-256-GCM encrypted and are never returned: GET lists keys with masked values only.
Credential keys must start with a letter and contain only letters, numbers, and underscores (max 50 chars). Values are capped at 1000 chars. If server-side encryption is not configured, the write is rejected with 503 rather than storing a secret in plaintext.
curl -X POST https://test-lab.ai/api/v1/credentials \
-H "Authorization: Bearer tl_xxxxx" \
-H "Content-Type: application/json" \
-d '{ "key": "password", "value": "hunter2" }'A plan prompt then references it as {{credentials.password}}.
Data fixtures (test data)
Reusable, generated test data. A fixture is a named bundle of typed fields; reference a field in a prompt as {{data.<fixtureKey>.<fieldKey>}}.
POST /api/v1/data-fixtures { "key": "...", "label"?: "...", "fields": [...] }
GET /api/v1/data-fixturesEach field is { key, value?, generator?, mode? }:
- static (default): a literal
value(may template{{run.X}}for per-run uniqueness). - dynamic (
"mode": "dynamic"): ageneratorrolls a fresh value on every run. Dynamic fields require a generator.
curl -X POST https://test-lab.ai/api/v1/data-fixtures \
-H "Authorization: Bearer tl_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"key": "newUser",
"label": "A fresh signup",
"fields": [
{ "key": "firstName", "mode": "dynamic", "generator": "person.firstName" },
{ "key": "email", "mode": "dynamic", "generator": "internet.email" },
{ "key": "plan", "mode": "static", "value": "pro" }
]
}'A plan prompt then references it as {{data.newUser.email}}.
Generators (for dynamic fields): person.firstName, person.lastName, person.fullName, person.jobTitle, internet.email, internet.username, internet.url, internet.ipv4, phone.number, location.streetAddress, location.city, location.state, location.zipCode, location.country, company.name, lorem.word, lorem.sentence, lorem.paragraph, date.past, date.future, string.uuid, string.alphanumeric, string.numeric, number.int, boolean.
Fixture and field keys: start with a letter; letters/digits/underscores; max 50 chars. Max 50 fields per fixture; static values capped at 4000 chars; max 200 fixtures per account.
Error responses
| Status | Meaning |
|---|---|
400 | Missing/invalid field, or an input cap exceeded |
401 | Missing or invalid API key |
403 | Account blocked (ACCOUNT_BLOCKED) or signed-in blocked (SIGNIN_BLOCKED) |
404 | A referenced pre-step plan was not found in your account |
409 | A label/fixture with that name/key already exists |
503 | Credential storage unavailable (encryption not configured) |