Error Reference

Error Reference

All Lathe Studio API errors use one envelope, so you can handle them in one place.


Error Envelope#

Every 4xx and 5xx response returns JSON with this shape:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "details": {
      "field": "name"
    }
  }
}
FieldTypeDescription
error.codestringMachine-readable error code. Branch on this, and on details.reason where present.
error.messagestringHuman-readable description. It can change between releases; don't parse it.
error.detailsobjectOptional. Extra context, such as the field that failed or a reason.

An endpoint's code for a given failure never changes. New detail is added in details.reason.


Error Codes#

StatusCodeWhen it occurs
400BAD_REQUESTA parameter or body value is missing or malformed (bad UUID, unknown field value, …)
400VALIDATION_ERRORThe body failed schema validation
400INVALID_JSONThe body is not valid JSON
401AUTH_REQUIREDMissing, malformed, revoked or expired API key
403FORBIDDENThe key lacks the required scope, is scoped to a different project, or the organization is locked (preview ended or subscription cancelled)
404RESOURCE_NOT_FOUNDThe resource does not exist, or belongs to another organization
409CONFLICTThe request conflicts with existing state (for example an idempotency key reused with a different body)
413PAYLOAD_TOO_LARGEThe request body is over the endpoint's size limit
422VALIDATION_ERRORThe request is well-formed but semantically invalid
429RATE_LIMITEDPer-minute rate limit or monthly quota exceeded
500INTERNAL_ERRORUnexpected server error

Error Examples#

401 — Missing or invalid key

{
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Missing or invalid Authorization header. Use: Bearer <api_key>"
  }
}

Check that your header is formatted as Authorization: Bearer pt_live_... (with a space after Bearer).

403 — Missing scope

{
  "error": {
    "code": "FORBIDDEN",
    "message": "API key missing required scope: builds:create"
  }
}

Create a new API key with the required scopes. Scopes cannot be added to existing keys.

403 — Organization locked

When the organization's 90-day preview has ended, every request is refused with 403 FORBIDDEN. When a paid subscription was cancelled, the organization is read-only: GET requests keep working and writes get 403 FORBIDDEN. An organization admin can subscribe from Settings → Billing in the app.

404 — Resource not found

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Build not found: 6f1c…",
    "details": { "resource": "Build", "id": "6f1c…" }
  }
}

The resource either doesn't exist or belongs to a different organization. The two cases get the same response.

429 — Rate limited

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded",
    "details": { "reset_at": 1790400000 }
  }
}

reset_at and the X-RateLimit-Reset header are Unix timestamps in seconds. See Rate Limits for retry guidance.

500 — Internal error

{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  }
}

Retry with exponential backoff. If the error persists, contact support@lathe.studio with the timestamp and endpoint.

400 — Validation details

Some 400 responses carry a machine-readable details.reason. One example: POST /api/v1/testruns accepts both release_id and build_id, but a run's release is its build's release — when the two disagree the request is refused rather than silently overridden:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "release_id does not match the build's release",
    "details": { "reason": "release_build_mismatch" }
  }
}

Send only build_id to place the run in the build's release (or no release, for a release-less build).

Assignment endpoints (POST /testruns with assignee_id/assignee_email, POST /testruns/{id}/assign, and splitting with an assignee) branch on details.reason too (touchless T4, 2026-09-24): assignee_not_found (the email matches no one in your organization — the identical response whether it belongs to another org or to no one), assignee_not_assignable (the member cannot be assigned test runs in this project), assignee_ambiguous (the email matches more than one user — retry with assignee_id), assignee_conflict (both assignee_id and assignee_email sent), assignee_and_queue (an assignee combined with queue: true), and invalid_assignee_email (malformed email address).


Idempotency#

For POST requests, include an Idempotency-Key header to safely retry without creating duplicate resources:

Idempotency-Key: my-pipeline-run-12345

If a request with the same key succeeds, subsequent identical requests return the original response (200 with the original resource). If the body differs, a 409 Conflict is returned (details.reason: idempotency_key_reused); a concurrent request with the same key still in flight returns 409 with details.reason: idempotency_request_in_progress.

Idempotency keys expire after 24 hours.

Endpoints that honour Idempotency-Key:

EndpointSince
POST /api/v1/builds2026-09-24

Other POST endpoints currently ignore the header (today's behaviour — no error is raised for sending one). This table is updated as endpoints gain support.