REST API

Errors

Error envelope, status codes, and validation details.

Envelope

Every non-success response is JSON:

{ "error": "Human-readable English message" }

Validation failures (422) include a details payload that matches Zod's .flatten() shape:

{
  "error": "Validation failed",
  "details": {
    "formErrors": [],
    "fieldErrors": {
      "name": ["String must contain at least 1 character(s)"]
    }
  }
}

The API resolves any internal translation keys to English before responding — clients always receive ready-to-display copy.

Status codes

StatusWhen
400 Bad RequestMalformed JSON, invalid Idempotency-Key header, or domain precondition like passing a stage from a different workspace.
401 UnauthorizedMissing, malformed, expired, revoked, or unknown API key.
403 ForbiddenAuthenticated, but the calling key lacks the role or project access required for this action.
404 Not FoundThe resource does not exist, or it belongs to a different organization. Cross-tenant IDs are always reported as 404, never 403, to avoid leaking existence.
409 ConflictIdempotency-Key was reused with a different body. Use a new key.
412 Precondition FailedDomain rule prevented the operation (e.g. finalizing an already-finalized subtask, moving a task into a workspace with no stages).
422 Unprocessable EntityZod validation failed — see details.
500 Internal Server ErrorUnhandled error. Check the server logs.

Handling validation errors

details.fieldErrors is an object keyed by field path. Each value is a list of human messages for that field. details.formErrors carries cross-field errors (e.g. "exactly one of taskId or subtaskId is required" on POST /time-entries).

Recommended approach:

const res = await fetch("/api/v1/projects", { ... });
if (res.status === 422) {
  const body = await res.json();
  showFieldErrors(body.details.fieldErrors);
  showFormErrors(body.details.formErrors);
  return;
}

Retrying safely

  • Idempotent verbs (GET, PATCH, DELETE) can be retried freely.
  • POST should always carry an Idempotency-Key if you intend to retry. See Idempotency.
  • Transient 5xx responses are not persisted by the idempotency layer, so retrying with the same key after a 5xx will re-execute the handler (this is intentional — the original write may have failed).

Authorization failures

  • 403 Forbidden is returned when the API key's user is not an org owner/admin for endpoints that require it (e.g. POST /projects, DELETE /projects/{id}).
  • For project-restricted resources (tasks within a project the user is not a member of), the API returns 404 — the user cannot see the resource at all.

On this page