API Changelog

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}/testcases accept test_case_type: "gherkin" together with gherkin_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 400 VALIDATION_ERROR and a line number in the message.
  • Gherkin cases own their steps through the text: sending steps or preconditions for a Gherkin case is a 400, and gherkin_content on 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.
  • GET on the test case list and detail has returned test_case_type and gherkin_content since 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 /organizations returns one object in data (plan, trialDaysLeft, isPaid, lockState, isExpired, billing, usage). It was documented as an array with slug and tier.
  • GET /events returns events[] with name, description and payload, plus docs_url and openapi_url. There was never an availability field, and this endpoint needs no key.
  • GET /releases/{id}/progress returns { release, builds, progress }, with progress.pass_rate_percent as an integer from 0 to 100. It was documented as a flat object with a 0–1 pass_rate.
  • GET /testruns/{id} wraps the run in { "data": … }. Runs also carry run_sequence_number, display_code and custom_environment, and environment is free text.
  • GET /testruns/{id}/results is paginated (limit, offset, result filter). Each item embeds its test_cases object; there is no top-level test_case_id, defect_refs or executed_by.
  • Builds: the queued status 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_number and display_code are documented; project_id and description were 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 an app. 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/ingest errors now use the standard envelope. It was the only route returning a flat { "error": "<text>", "message" }. Messages are unchanged; 400s add details.field, and 409s add details.reason: "release_completed".
  • POST /testruns/{id}/results now stores duration_seconds. Before, it was validated and then dropped.
  • POST /testruns/{id}/results no longer writes the raw result into effective_status, which is the inheritance engine's verdict.
  • The Idempotency-Key header was documented on POST /testruns/{id}/results but 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, options as labels, position; archived omitted). Requires projects: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}/testcases and GET …/testcases/{tcId} now include custom_fields on every case, in the P6 wire format: { "<key>": value } with select values as option labels and multi_select as an array of labels. Values whose definition has been archived are omitted — the API describes the current schema.
  • POST/PUT on test cases accept custom_fields. PUT merges — unspecified keys keep their stored values, an explicit null clears a key. (Until now the API refused custom_fields with 400 not_supported_until_custom_fields_s4; that temporary rejection is gone.)
  • Write failures are 400 BAD_REQUEST, branched on details.reason: custom_field_unknown (key is not an active field of this project — archived counts as unknown; details.field names 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_type field from the TestCase schema and examples. It had no database column; the live API returns test_case_type (Standard/Gherkin/Flow format) and case_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/offset pagination, optional status filter). Requires the new releases:read scope.
  • GET /api/v1/releases/{id} — a release with its build_count. Requires releases:read. (Until now the only release endpoint was GET /releases/{id}/progress.)
  • POST /api/v1/releases — create a release (project_key?, name, description?, target_date?, status?). Requires the new releases:create scope. Returns 201 + Location; honours Idempotency-Key exactly like POST /builds (identical retry → 200 replay, same key different body → 409).
  • GET /api/v1/builds/{id} — fetch one build. Requires builds:read. (The Location header POST /builds has 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 new builds:update scope. Body is exactly release_id; a completed release refuses the move in either direction (409 details.reason: 'release_completed'), and a name collision in the target release suffixes the build name ("X (2)") instead of refusing.
  • GET /api/v1/builds — new release filter (none for release-less builds, a release UUID otherwise) and a project_key alternative to project_id (both sent and disagreeing → 400).

Scopes

  • New scopes: releases:read, releases:create, and builds: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 /releases refuses status: 'completed' with 400 VALIDATION_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}/assign now returns 409 CONFLICT with details.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}/split refuses 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, and POST /api/v1/testruns/{id}/split now accept assignee_email anywhere assignee_id is accepted. The email is trimmed, validated, and resolved case-insensitively to a user inside the API key's organization; the assignment then behaves exactly like assignee_id (same validation, notifications attributed to the API key, and testruns:assign scope 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 on users.email — disambiguate with assignee_id), assignee_conflict (both assignee fields sent), and invalid_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 new testruns:assign scope. Idempotent: repeating the current target returns 200 with changed: false and no side effects.
  • POST /api/v1/testruns — accepts queue: true to create a run directly in the unassigned queue (mutually exclusive with assignee_id: 400 VALIDATION_ERROR, details.reason: 'assignee_and_queue').

Scopes

  • New scope: testruns:assign — required for POST /testruns/{id}/assign, and now also required to create a run with assignee_id or queue: true, or to split a run with assignee_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.v1 webhook, an email to the assignee, and the run.assigned Slack/Teams trigger. This covers POST /testruns/{id}/assign, creating a run with assignee_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_by is null and the new additive assigned_by_api_key: { id, name } is set in the webhook payload; audit rows carry the key in metadata with no user_id).

v1.0.0 — March 2026#

Initial API release.

Endpoints

  • POST /api/v1/builds — Create a build from your CI/CD pipeline
  • GET /api/v1/builds — List builds with pagination and filtering
  • GET /api/v1/testruns/{id} — Get a test run by ID
  • GET /api/v1/testruns/{id}/results — Get execution results for a test run
  • POST /api/v1/results/ingest — Ingest automated test results from CI/CD pipelines
  • GET /api/v1/projects — List projects
  • GET /api/v1/projects/{id}/testcases — List test cases (for automation framework sync)
  • GET /api/v1/releases/{id}/progress — Release readiness summary
  • GET /api/v1/organizations — List organizations
  • GET /api/v1/events — Discover supported webhook event types
  • GET /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:update
    • testruns:read, testruns:create
    • results:read, results:submit
    • projects:read

v1.1.0 — May 2026#

Endpoints

  • GET /api/v1/testruns — List test runs
  • POST /api/v1/testruns — Create a test run via API
  • POST /api/v1/testruns/{id}/results — Submit automation results directly to a test run
  • GET /api/v1/webhooks — List webhook subscriptions
  • POST /api/v1/webhooks — Create webhook subscription
  • DELETE /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 created
  • testrun.submitted.v1 — Test run submitted
  • testrun.completed.v1 — Test run marked completed
  • testcase.failed.v1 — Test case execution records a fail
  • release.released.v1 — Release status changes to completed

Features:

  • HMAC-SHA256 signature verification (X-Lathe-Signature header)
  • 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:

  1. A deprecation notice is published in this changelog and announced via email to billing owners
  2. The deprecated behavior remains functional for a minimum of 6 months
  3. A sunset date is published at least 60 days in advance
  4. 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.