API Changelog
This page tracks changes to the Lathe Studio API. We follow a 6-month deprecation window for breaking changes — deprecated endpoints and fields remain functional for at least 6 months after the deprecation notice, with a sunset date published in advance.
Unreleased — 2026-10-02 (Gherkin format in the API)#
Behaviour notes
POST/PUT/projects/{id}/testcasesaccepttest_case_type: "gherkin"together withgherkin_content(the Given/When/Then source text). The steps are derived from the text by the same core the browser uses, so an API-written case executes identically to a UI-written one. Invalid text is refused with 400VALIDATION_ERRORand a line number in the message.- Gherkin cases own their steps through the text: sending
stepsorpreconditionsfor a Gherkin case is a 400, andgherkin_contenton a Standard case is a 400. A case's Format does not change after creation — Gherkin cannot be switched to Standard or back through the API. GETon the test case list and detail has returnedtest_case_typeandgherkin_contentsince the format shipped; both are now documented on the write side as well.
Unreleased — 2026-09-26 (contract accuracy: docs match the API)#
The OpenAPI spec (now 1.2.0), the Postman collection and the API guides were corrected to describe what the API actually returns. Most of this entry is documentation only. The four API changes are listed separately under Behaviour changes.
Documentation corrections
- Errors are
{ "error": { "code", "message", "details"? } }with uppercase codes (BAD_REQUEST,VALIDATION_ERROR,AUTH_REQUIRED,FORBIDDEN,RESOURCE_NOT_FOUND,CONFLICT,RATE_LIMITED,INTERNAL_ERROR…). The old docs showed a flat{ "error": "invalid_request" }that no route returned. GET /organizationsreturns one object indata(plan,trialDaysLeft,isPaid,lockState,isExpired,billing,usage). It was documented as an array withslugandtier.GET /eventsreturnsevents[]withname,descriptionandpayload, plusdocs_urlandopenapi_url. There was never anavailabilityfield, and this endpoint needs no key.GET /releases/{id}/progressreturns{ release, builds, progress }, withprogress.pass_rate_percentas an integer from 0 to 100. It was documented as a flat object with a 0–1pass_rate.GET /testruns/{id}wraps the run in{ "data": … }. Runs also carryrun_sequence_number,display_codeandcustom_environment, andenvironmentis free text.GET /testruns/{id}/resultsis paginated (limit,offset,resultfilter). Each item embeds itstest_casesobject; there is no top-leveltest_case_id,defect_refsorexecuted_by.- Builds: the
queuedstatus is documented, and the list and detail shapes no longer claim CI fields (commit_sha,branch, …) that are stored but not returned. - Test cases:
test_case_type,tc_sequence_numberanddisplay_codeare documented;project_idanddescriptionwere never returned. - Every operation now lists the 400/404/409/500 responses it can actually send.
- The production base URL is
https://lathe.studio/api/v1. The spec, the Postman collection and the developer skill named anapp.subdomain, which is not a host we use. - The Postman collection is generated from the spec. It gained the endpoints it was missing and lost requests for endpoints that never existed.
Behaviour changes
POST /results/ingesterrors now use the standard envelope. It was the only route returning a flat{ "error": "<text>", "message" }. Messages are unchanged; 400s adddetails.field, and 409s adddetails.reason: "release_completed".POST /testruns/{id}/resultsnow storesduration_seconds. Before, it was validated and then dropped.POST /testruns/{id}/resultsno longer writes the raw result intoeffective_status, which is the inheritance engine's verdict.- The
Idempotency-Keyheader was documented onPOST /testruns/{id}/resultsbut never honoured, and now isn't documented there.result: "retest"has always been accepted and is now documented.
Unreleased — 2026-09-25 (custom fields S4: custom fields in the API)#
Endpoints
GET /api/v1/projects/{id}/custom-fields— the project's active custom field definitions in display order (key,label,field_type,optionsas labels,position; archived omitted). Requiresprojects:read. There is intentionally no create/edit/archive endpoint for definitions — managing fields stays a human task in Project Settings.GET /api/v1/projects/{id}/testcases/{tcId}— fetch one test case (new detail read; previously only the list existed).
Behaviour notes
GET /projects/{id}/testcasesandGET …/testcases/{tcId}now includecustom_fieldson every case, in the P6 wire format:{ "<key>": value }withselectvalues as option labels andmulti_selectas an array of labels. Values whose definition has been archived are omitted — the API describes the current schema.POST/PUTon test cases acceptcustom_fields. PUT merges — unspecified keys keep their stored values, an explicitnullclears a key. (Until now the API refusedcustom_fieldswith 400not_supported_until_custom_fields_s4; that temporary rejection is gone.)- Write failures are 400
BAD_REQUEST, branched ondetails.reason:custom_field_unknown(key is not an active field of this project — archived counts as unknown;details.fieldnames the key),custom_field_invalid(value doesn't fit the type or isn't an option label),custom_fields_not_object. Validation is the same core the browser uses — a case edited in the UI and one written through the API store identical values. - Removed the phantom
test_typefield from theTestCaseschema and examples. It had no database column; the live API returnstest_case_type(Standard/Gherkin/Flow format) andcase_type(vendor-style classification).
Unreleased — 2026-09-24 (touchless T5: releases and release-less builds in the API)#
Endpoints
GET /api/v1/releases— list a project's releases (newest first,limit/offsetpagination, optionalstatusfilter). Requires the newreleases:readscope.GET /api/v1/releases/{id}— a release with itsbuild_count. Requiresreleases:read. (Until now the only release endpoint wasGET /releases/{id}/progress.)POST /api/v1/releases— create a release (project_key?,name,description?,target_date?,status?). Requires the newreleases:createscope. Returns201+Location; honoursIdempotency-Keyexactly likePOST /builds(identical retry →200replay, same key different body →409).GET /api/v1/builds/{id}— fetch one build. Requiresbuilds:read. (TheLocationheaderPOST /buildshas returned since T1 finally resolves.)PATCH /api/v1/builds/{id}— place a build into a release, or back to "No release" with{ "release_id": null }. Requires the newbuilds:updatescope. Body is exactlyrelease_id; a completed release refuses the move in either direction (409details.reason: 'release_completed'), and a name collision in the target release suffixes the build name ("X (2)") instead of refusing.GET /api/v1/builds— newreleasefilter (nonefor release-less builds, a release UUID otherwise) and aproject_keyalternative toproject_id(both sent and disagreeing → 400).
Scopes
- New scopes:
releases:read,releases:create, andbuilds:update(re-added — it was deleted on 2026-09-20 when no route required it;PATCH /builds/{id}now gives it meaning). Existing API keys do not receive them automatically (product decision D3, 2026-09-24) — an admin enables them under Org Settings → API Keys.
Behaviour notes
POST /releasesrefusesstatus: 'completed'with 400VALIDATION_ERROR,details.reason: 'release_status_not_allowed'— creating an already-completed release is completing one, and completing stays human-only. There is no release update/status/complete/delete endpoint, by design.- Duplicate release names are refused, not suffixed: 409
details.reason: 'release_name_taken'(same rule as the UI; the auto-suffixing is a builds-only rule).
Unreleased — 2026-09-24 (touchless T3b: completed release freezes run assignment)#
Behaviour changes
- Once a run's release is
completed, the run's assignment state cannot change by any path — assign, reassign, unassign, queue, or a tester's grab/release.POST /api/v1/testruns/{id}/assignnow returns 409CONFLICTwithdetails.reason: 'release_completed'for every body shape (assignee_id,assignee_email,queue: true,unassigned: true), even when the target equals the run's current state (a retry after completion gets a 409, not a silent 200). POST /api/v1/testruns/{id}/splitrefuses a run whose release is completed with the same 409 (details.reason: 'release_completed') — splitting creates runs, and run creation is already refused for a completed release.- Runs in open releases, and runs with no release, behave exactly as before.
Unreleased — 2026-09-24 (touchless T4: assignee by email, D4)#
Endpoints
POST /api/v1/testruns,POST /api/v1/testruns/{id}/assign, andPOST /api/v1/testruns/{id}/splitnow acceptassignee_emailanywhereassignee_idis accepted. The email is trimmed, validated, and resolved case-insensitively to a user inside the API key's organization; the assignment then behaves exactly likeassignee_id(same validation, notifications attributed to the API key, andtestruns:assignscope requirement).- Failure reasons on 400
VALIDATION_ERROR:assignee_not_found(identical response whether the email belongs to another organization or to no one — the API never confirms which),assignee_not_assignable,assignee_ambiguous(email matches more than one user; no unique constraint onusers.email— disambiguate withassignee_id),assignee_conflict(both assignee fields sent), andinvalid_assignee_email. - There is intentionally no endpoint that lists organization members or their emails in this phase (owner decision D4) — an API key cannot read the org's email list.
Unreleased — 2026-09-24 (touchless T3: run assignment through one core)#
Endpoints
POST /api/v1/testruns/{id}/assign— assign a run to a user, release it into the unassigned queue ({ "queue": true }), or pull it back to plain unassigned ({ "unassigned": true }). Requires the newtestruns:assignscope. Idempotent: repeating the current target returns200withchanged: falseand no side effects.POST /api/v1/testruns— acceptsqueue: trueto create a run directly in the unassigned queue (mutually exclusive withassignee_id: 400VALIDATION_ERROR,details.reason: 'assignee_and_queue').
Scopes
- New scope:
testruns:assign— required forPOST /testruns/{id}/assign, and now also required to create a run withassignee_idorqueue: true, or to split a run withassignee_id. Existing API keys do not receive the scope automatically (product decision D3, 2026-09-24) — an admin enables it under Org Settings → API Keys. Keys that create runs with an assignee and lack the scope now receive 403 (previously 201).
Behaviour changes (assignment notifications)
- API-driven assignments now send the same notifications as UI assignments: the
testrun.assigned.v1webhook, an email to the assignee, and therun.assignedSlack/Teams trigger. This coversPOST /testruns/{id}/assign, creating a run withassignee_id, and splitting a run with an assignee. Previously none of these paths sent anything. - Notifications and audit rows attribute the actor: UI assignments name the user; API assignments name the API key (
assigned_byisnulland the new additiveassigned_by_api_key: { id, name }is set in the webhook payload; audit rows carry the key inmetadatawith nouser_id).
v1.0.0 — March 2026#
Initial API release.
Endpoints
POST /api/v1/builds— Create a build from your CI/CD pipelineGET /api/v1/builds— List builds with pagination and filteringGET /api/v1/testruns/{id}— Get a test run by IDGET /api/v1/testruns/{id}/results— Get execution results for a test runPOST /api/v1/results/ingest— Ingest automated test results from CI/CD pipelinesGET /api/v1/projects— List projectsGET /api/v1/projects/{id}/testcases— List test cases (for automation framework sync)GET /api/v1/releases/{id}/progress— Release readiness summaryGET /api/v1/organizations— List organizationsGET /api/v1/events— Discover supported webhook event typesGET /api/v1/openapi— OpenAPI spec discovery (redirects to/openapi.json)
Authentication
- API key authentication (Bearer token,
pt_live_<hex>format) - Scope-based access control:
builds:read,builds:create,builds:updatetestruns:read,testruns:createresults:read,results:submitprojects:read
v1.1.0 — May 2026#
Endpoints
GET /api/v1/testruns— List test runsPOST /api/v1/testruns— Create a test run via APIPOST /api/v1/testruns/{id}/results— Submit automation results directly to a test runGET /api/v1/webhooks— List webhook subscriptionsPOST /api/v1/webhooks— Create webhook subscriptionDELETE /api/v1/webhooks/{id}— Delete webhook subscription
Webhooks
Outgoing webhooks are now live.
Subscribable events:
build.created.v1— New build created (UI or API)testrun.created.v1— New test run createdtestrun.submitted.v1— Test run submittedtestrun.completed.v1— Test run marked completedtestcase.failed.v1— Test case execution records a failrelease.released.v1— Release status changes to completed
Features:
- HMAC-SHA256 signature verification (
X-Lathe-Signatureheader) - Automatic retries with exponential backoff (5 attempts over ~14 hours)
- Delivery log stored for audit and debugging
Breaking Change Policy#
We take backward compatibility seriously. Before making any breaking change:
- A deprecation notice is published in this changelog and announced via email to billing owners
- The deprecated behavior remains functional for a minimum of 6 months
- A sunset date is published at least 60 days in advance
- After sunset, the old behavior is removed in a minor version increment
What counts as a breaking change:
- Removing an endpoint
- Removing a required or optional request field
- Changing the type of a response field
- Changing the meaning of an error code
- Removing a scope
What does NOT count as a breaking change:
- Adding new optional request fields
- Adding new response fields
- Adding new endpoints
- Adding new error codes
- Adding new webhook event types
- Fixing incorrect behavior that was clearly a bug
Versioning#
The API uses URL path versioning: /api/v1/, /api/v2/, etc.
Within a major version, all changes are backward-compatible. A new major version (e.g., /api/v2/) is only released for breaking changes, and both versions are supported simultaneously for at least 12 months.