Test Plans API
Test plans are project-scoped collections of selected test cases. A plan holds an ordered selection of live cases; from that selection you can generate a test run that snapshots the cases at the moment of generation. Each plan has a server-assigned ULID, a monotonically allocated plan number within the project, and an optional description.
Create
Section titled “Create”/api/v1/projects/{projectId}/plans{projectId} is the project code (for example ACME).
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
name | yes | Non-empty string, max 200 characters. Must be unique among live plans in the project |
description | no | Free-text description, max 2000 characters |
status | no | Initial status: draft, active, or completed. Defaults to draft when omitted |
Client-supplied ulid or planNumber fields are rejected. Duplicate live name within the project returns 409 conflict. Missing or invalid fields return 422 validation_failed.
Response
Section titled “Response”201 with the created plan object. See Response fields.
Example
Section titled “Example”curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Sprint 24","description":"Feature release"}' \ "https://probara.net/api/v1/projects/ACME/plans"/api/v1/projects/{projectId}/plansReturns the live (non-deleted) plans for the project, ordered by plan number ascending. Page-based pagination.
Query parameters
Section titled “Query parameters”| Parameter | Notes |
|---|---|
page | Page number (1-based, default 1) |
pageSize | Items per page (default 20, max 200) |
Response
Section titled “Response”200 with { items, page, pageSize, total }. Each item follows the response shape.
Example
Section titled “Example”curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/projects/ACME/plans?page=1&pageSize=20"/api/v1/plans/{planUlid}Returns a single live plan scoped to the active organization, including the current case count.
Returns 404 not_found when the plan is outside the active organization or has been deleted.
Example
Section titled “Example”curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"Update
Section titled “Update”/api/v1/plans/{planUlid}Updates one or more fields of a live plan. At least one field must be provided. Duplicate live name returns 409 conflict.
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
name | no | New name (max 200 characters). Must be unique among live plans in the project |
description | no | Updated description (max 2000 characters). Send null to clear |
status | no | New lifecycle status: draft, active, or completed. Any transition between distinct statuses is valid. Sending the plan’s current status is a no-op (accepted, no event emitted). An unknown status literal returns 422 validation_failed |
milestoneId | no | ULID of a live milestone in the plan’s project to link, or null to clear the existing link. A ULID that does not resolve to a live milestone in the plan’s project returns 404 not_found and leaves the plan unchanged. A milestone from another project or organization is also 404 not_found |
An empty body (no fields) returns 422 validation_failed.
Response
Section titled “Response”200 with the updated plan object. See Response fields.
Status transition rules
Section titled “Status transition rules”- All transitions between
draft,active, andcompletedare valid in any direction. - There are no terminal states: a
completedplan can be moved back toactiveordraft. - A same-value status PATCH (the
statusfield equals the plan’s current status) is accepted as a no-op — no status change is recorded and the plan returns200unchanged. - An unknown status literal (not one of the three valid values) returns
422 validation_failed. - Status is purely manual: the API never auto-advances a plan’s status based on its runs.
Examples
Section titled “Examples”# Change status to activecurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status":"active"}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"# Link to a milestonecurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"milestoneId":"01HZMILESTONE0000000000000"}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"# Clear the milestone linkcurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"milestoneId":null}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"# Combined: rename and mark completedcurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Sprint 25 Final","status":"completed"}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"Delete
Section titled “Delete”/api/v1/plans/{planUlid}Soft-deletes the plan. The plan is no longer listed or accessible, and its name becomes available for reuse. Existing runs generated from the plan are not affected.
Returns 204 on success, 404 not_found when not found.
Example
Section titled “Example”curl -sS -X DELETE \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000"Replace the case selection
Section titled “Replace the case selection”/api/v1/plans/{planUlid}/casesReplace-set semantics: the entire selection is replaced with the supplied ordered list of case ULIDs. Omitting a case from the list removes it; supplying it in a different position moves it. Send an empty caseUlids array to clear the selection.
You can optionally assign a team member to each case in the same call by supplying caseAssignees. Assignee entries whose caseUlid is not present in caseUlids are silently ignored. All assignee resolution happens before the selection is replaced — a bad assigneeUlid leaves the current selection unchanged.
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
caseUlids | yes | Ordered array of live test case ULIDs. Duplicate ULIDs rejected (422). Cases must belong to the plan’s project |
caseAssignees | no | Array of { caseUlid, assigneeUlid } pairs. Each assigneeUlid must be a member of the active organization — a cross-org assigneeUlid returns 404 not_found and leaves the selection unchanged. Entries whose caseUlid is not in caseUlids are ignored. Omitting this field (or omitting an entry for a given case) stores that case unassigned |
Returns 404 not_found when any supplied ULID is not a live case in the project, or when any assigneeUlid is not an org member. Returns 422 validation_failed for duplicate ULIDs.
Response
Section titled “Response”200 with { items } — the new selection in position order. See Selection item fields.
Example — case selection only
Section titled “Example — case selection only”curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"caseUlids":["01HZCASEA","01HZCASEB","01HZCASEC"]}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/cases"Example — case selection with per-case assignees
Section titled “Example — case selection with per-case assignees”curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "caseUlids": ["01HZCASEA","01HZCASEB","01HZCASEC"], "caseAssignees": [ { "caseUlid": "01HZCASEA", "assigneeUlid": "01HZMEMBER000000000000000001" }, { "caseUlid": "01HZCASEB", "assigneeUlid": "01HZMEMBER000000000000000002" } ] }' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/cases"01HZCASEC has no entry in caseAssignees and is stored unassigned.
Update a plan case’s assignee
Section titled “Update a plan case’s assignee”/api/v1/plans/{planUlid}/cases/{planCaseUlid}Updates the assignee of a single plan case without modifying the rest of the case selection. This endpoint is the per-case inline update — it does not replace the plan’s case list.
Snapshot semantics: updating a plan case’s assignee does not back-propagate to any runs already generated from the plan. Existing run cases keep the assignee they had at the moment the run was generated.
Path parameters
Section titled “Path parameters”| Parameter | Description |
|---|---|
planUlid | ULID of the plan |
planCaseUlid | ULID of the plan case row (the ulid field from a selection item, not the test case ULID) |
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
assigneeUlid | yes | ULID of a live org member, or null to clear the assignee |
The body must be exactly one key. Unknown keys return 422 validation_failed.
Response
Section titled “Response”200 with the updated plan case object (same shape as a selection item). See Selection item fields.
Error codes
Section titled “Error codes”| Status | Code | When |
|---|---|---|
403 Forbidden | — | Caller has viewer role |
404 Not Found | not_found | planUlid or planCaseUlid does not exist or belongs to a different org |
404 Not Found | not_found | assigneeUlid does not resolve to a live org member |
422 Unprocessable Entity | validation_failed | Missing assigneeUlid key, non-ULID value, or unknown key |
Example
Section titled “Example”# Assign a member to a plan casecurl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"assigneeUlid":"01HZMEMBER000000000000000001"}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/cases/01HZPLANCASE000000000000000"
# Clear the assignee (send null)curl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"assigneeUlid":null}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/cases/01HZPLANCASE000000000000000"List the case selection
Section titled “List the case selection”/api/v1/plans/{planUlid}/casesReturns the current case selection for the plan, ordered by position ascending.
Response
Section titled “Response”200 with { items }. See Selection item fields.
Example
Section titled “Example”curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/cases"Replace the configuration selection
Section titled “Replace the configuration selection”/api/v1/plans/{planUlid}/configurationsReplace-set semantics: the plan’s entire configuration selection is replaced with the supplied list of configuration value ULIDs. Send an empty configurationUlids array to clear the selection.
Each supplied ULID must resolve to a live (non-deleted) configuration value in the plan’s project. An unresolved, soft-deleted, or cross-project ULID returns 404 not_found and leaves the selection unchanged. Duplicate ULIDs in the request return 422 validation_failed.
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
configurationUlids | yes | Array of live configuration value ULIDs. Duplicates rejected (422). Values must belong to the plan’s project |
Response
Section titled “Response”200 with { items } — the new configuration selection. See Configuration selection item fields.
Example
Section titled “Example”curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"configurationUlids":["01HZCHROME000000000000000000","01HZWINDOWS00000000000000000"]}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/configurations"List the configuration selection
Section titled “List the configuration selection”/api/v1/plans/{planUlid}/configurationsReturns the plan’s current configuration selection — the set of configuration value ULIDs the plan uses to compute its combination matrix.
Response
Section titled “Response”200 with { items }. See Configuration selection item fields.
Example
Section titled “Example”curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/configurations"Set excluded combinations
Section titled “Set excluded combinations”/api/v1/plans/{planUlid}/excluded-combinationsReplace-set semantics: the plan’s excluded combination keys are replaced with exactly the supplied set. A comboKey that does not match any current candidate combination is accepted and stored as an inert exclusion — it will not cause an error.
Request body
Section titled “Request body”| Field | Required | Notes |
|---|---|---|
comboKeys | yes | Array of combination key strings. A combination key is the sorted, colon-joined list of the combination’s configuration value ULIDs (e.g. 01HZCHROME:01HZWINDOWS). Duplicates rejected (422). Send an empty array to clear all exclusions |
Response
Section titled “Response”200 with { comboKeys } — the plan’s new excluded combination key set.
Example
Section titled “Example”curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"comboKeys":["01HZFIREFOX0000000000000000:01HZWINDOWS00000000000000000"]}' \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/excluded-combinations"Combinations preview
Section titled “Combinations preview”/api/v1/plans/{planUlid}/combinationsReturns the candidate combinations computed as the cartesian product of the plan’s current configuration selection (one value per participating group). Each combination carries its comboKey, the ordered per-group values, and an included flag (false when the comboKey is in the plan’s excluded set). The response also returns the total cartesian count and the count of included combinations.
A plan with no configuration selection returns exactly one candidate combination (the empty combination, totalCount = 1, includedCount = 1).
Response
Section titled “Response”200 with:
| Field | Type | Notes |
|---|---|---|
items | array | Candidate combination objects. See Combination item fields |
totalCount | number | Total number of candidate combinations (including excluded) |
includedCount | number | Number of combinations with included = true |
Example
Section titled “Example”curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://probara.net/api/v1/plans/01HZPLANULID00000000000000/combinations"Creating a run for a plan
Section titled “Creating a run for a plan”There is no POST /api/v1/plans/{planUlid}/runs. Create a run for a plan with
POST /api/v1/projects/{projectId}/runs, passing planUlid and, for a configured
plan, the combination’s configurationUlids. This call always creates exactly
one run — link a plan, seed its cases, and tag one combination, all in a single
request. To cover several combinations, call it once per combination.
When planUlid is supplied and caseUlids is omitted, the run’s case selection and
per-case assignees are seeded from the plan’s current selection, following this
precedence (first match wins):
- An explicit per-case entry in
caseAssignees(the request body) - The plan’s stored per-case assignee for that case
defaultAssigneeUlidfrom the request body- Unassigned (
null)
A plan assignee who is no longer a member of the organization at request time seeds
to null (unassigned) without failing the request.
curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"Smoke — Chrome","planUlid":"01HZPLANULID00000000000000","configurationUlids":["01HZCFGVALCHROME0000000000"]}' \ "https://probara.net/api/v1/projects/PROJ/runs"The plan’s runs are listed most-recent first via GET /api/v1/plans/{planUlid}/runs.
Response fields
Section titled “Response fields”The plan detail (GET /api/v1/plans/{planUlid}) and the PATCH response return the full plan object. The plan list (GET /api/v1/projects/{projectId}/plans) returns the same shape per item but omits progress.
| Field | Type | Notes |
|---|---|---|
ulid | string | Server-assigned ULID for the plan |
projectUlid | string | ULID of the project the plan belongs to |
planNumber | number | Monotonically allocated number within the project |
name | string | Plan name |
description | string | null | Optional description |
author | object | null | Authorship snapshot (see below). null for legacy rows |
caseCount | number | Current count of live cases in the selection |
status | string | Current lifecycle status: draft, active, or completed |
milestoneUlid | string | null | ULID of the linked milestone, or null when no milestone is linked |
milestone | object | null | Linked milestone reference ({ ulid, name }), or null when no milestone is linked |
progress | object | Computed progress rollup (detail and PATCH responses only — see below) |
createdAt | number | Creation timestamp (Unix epoch milliseconds) |
updatedAt | number | Last-update timestamp (Unix epoch milliseconds) |
Author fields
Section titled “Author fields”| Field | Type | Notes |
|---|---|---|
kind | "user" | "api_token" | How the plan was created |
ulid | string | ULID of the user or API token |
displayName | string | Display name (user kind only) |
avatarKey | string | null | Avatar object key (user kind only) |
name | string | Token name (api_token kind only) |
Progress fields
Section titled “Progress fields”progress is computed at read time from the plan’s runs. It is present on the plan detail (GET /api/v1/plans/{planUlid}) and the PATCH response. The plan list does not carry progress.
| Field | Type | Notes |
|---|---|---|
totalRuns | number | Total number of runs generated from this plan |
closedRuns | number | Number of those runs that are in closed state |
counts | object | Overall outcome counts summed across all the plan’s runs (see below) |
byConfiguration | array | Per-configuration breakdown rows (see below). Empty when the plan has no runs |
Outcome counts (counts and per-breakdown-row counts)
Section titled “Outcome counts (counts and per-breakdown-row counts)”| Field | Type | Notes |
|---|---|---|
passed | number | Sum of passed test results |
failed | number | Sum of failed test results |
blocked | number | Sum of blocked test results |
skipped | number | Sum of skipped test results |
untested | number | Sum of untested (pending) test results |
These are summed from the denormalized per-status counters maintained on each test_run row. No re-scanning of individual results occurs at read time.
Per-configuration breakdown (byConfiguration items)
Section titled “Per-configuration breakdown (byConfiguration items)”Each item in byConfiguration corresponds to one configuration combination (or the unconfigured bucket):
| Field | Type | Notes |
|---|---|---|
comboKey | string | Combination key: the run’s configuration value ULIDs sorted ascending and joined with :. Empty string ("") for the unconfigured bucket (runs with no configuration tags) |
values | array | Ordered per-group value entries ({ groupName, valueName }). Empty array for the unconfigured bucket |
totalRuns | number | Number of runs in this combination |
closedRuns | number | Number of those runs that are closed |
counts | object | Outcome counts for this combination (same shape as the top-level counts) |
The sum of all byConfiguration[*].counts equals the top-level counts. Runs with no configuration tags appear in a single unconfigured bucket (empty comboKey, empty values).
Selection item fields
Section titled “Selection item fields”Returned by GET /plans/{planUlid}/cases and PUT /plans/{planUlid}/cases.
| Field | Type | Notes |
|---|---|---|
ulid | string | Server-assigned ULID of the selection row |
caseUlid | string | ULID of the referenced test case |
displayId | string | Display ID of the case (e.g. TC-42) |
title | string | Current title of the test case |
suiteName | string | null | Suite name for grouping, or null |
position | number | 0-based position within the plan’s selection |
assigneeUlid | string | null | ULID of the member assigned to this case in the plan, or null when the case is unassigned |
Configuration selection item fields
Section titled “Configuration selection item fields”Each item in the GET /plans/{planUlid}/configurations and PUT /plans/{planUlid}/configurations responses:
| Field | Type | Notes |
|---|---|---|
configurationUlid | string | ULID of the configuration value |
groupUlid | string | ULID of the configuration group this value belongs to |
groupName | string | Display name of the group (e.g. Browser) |
valueName | string | Display name of the value (e.g. Chrome) |
Combination item fields
Section titled “Combination item fields”Each item in the GET /plans/{planUlid}/combinations response:
| Field | Type | Notes |
|---|---|---|
comboKey | string | Deterministic combination key: the combination’s configuration value ULIDs sorted ascending and joined with :. Use this key in PUT /excluded-combinations to exclude the combination |
values | array | Per-group value entries, ordered by group position. Each entry has groupUlid, groupName, configurationUlid, and valueName |
included | boolean | true when this combination is not in the plan’s excluded set; false when excluded |