Errors

Every failure uses the same envelope, so you can write one handler and trust it everywhere.

The envelope

json
{
  "error": {
    "code": "validation_failed",
    "message": "postcode is not a valid UK postcode",
    "details": [
      { "field": "buy_box.postcodes", "issue": "invalid_format" }
    ]
  }
}

code is stable and safe to branch on. message is human-readable and may change wording — never parse it. details is present on validation failures. The response also carries X-Request-Id; log it.

Status codes

FieldTypeDescription
400bad_requestMalformed JSON or an unusable query parameter.
401unauthenticatedMissing, expired or revoked session or key. Sign in again or rotate the key.
403forbidden / insufficient_scopeAuthenticated but the role or key scope does not allow this. Do not retry.
404not_foundNo such record in this workspace. Also returned for records in other workspaces.
409conflictThe record changed under you, or a unique value is taken. Re-read and retry.
422validation_failedThe body parsed but a field is unacceptable. See details.
429rate_limitedToo many requests. Back off using Retry-After.
5xxinternal_errorOur fault. Retry with backoff and send us the request ID if it persists.

CSRF failures

A browser request that omits or reuses a stale X-CSRF-Token returns 403 csrf_invalid. Re-fetch /ping to get a fresh token, then repeat the request once.

Handling errors in one place

ts
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "unauthenticated": return signInAgain();
    case "csrf_invalid":    return refreshCsrfAndRetryOnce();
    case "rate_limited":    return backoff(res.headers.get("Retry-After"));
    case "validation_failed": return showFieldErrors(error.details);
    default: throw new ApiError(error, res.headers.get("X-Request-Id"));
  }
}