Errors

Every error response uses the same envelope with a code from a small, closed set — so you can branch on the code reliably.

The error envelope

All errors return a JSON body of this shape:

{
  "error": {
    "code": "validation_failed",
    "message": "effectiveDate: must match ^\\d{4}-\\d{2}-\\d{2}$",
    "retryable": false
  }
}

Error codes

CodeHTTPMeaning
unauthorized401 Missing or invalid access token. Get a fresh token and retry.
forbidden403 The token is valid but lacks a required permission, or an x-principal-id was invalid. See Authentication.
not_found404 The resource doesn't exist, or isn't visible to your account. Returned in place of 403 where confirming existence would itself leak information.
validation_failed400 The request failed schema validation (a bad field, wrong type, missing required value). message names the offending field. Not retryable without fixing the request.
conflict409 The request conflicts with existing state — for example an Idempotency-Key reused with a different body, or a duplicate submission reference. See Async mutations.
internal_error500 An unexpected server-side error. retryable is true; retry with backoff.
rate_limited Reserved for future use. The current API does not emit this code; it's included so clients can handle it defensively without a breaking change if rate limiting is introduced later.

The set of codes is closed and stable: the API will not introduce a new code value within v1 without a version change. You can safely write an exhaustive switch over these values. Handling an unknown code gracefully (treating it like internal_error) is still good practice.

Errors on async mutations

Creating a job (for example POST /v1/submissions) can fail at two different times, and the same envelope is used for both: