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
| Status | When |
|---|---|
400 Bad Request | Malformed JSON, invalid Idempotency-Key header, or domain precondition like passing a stage from a different workspace. |
401 Unauthorized | Missing, malformed, expired, revoked, or unknown API key. |
403 Forbidden | Authenticated, but the calling key lacks the role or project access required for this action. |
404 Not Found | The 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 Conflict | Idempotency-Key was reused with a different body. Use a new key. |
412 Precondition Failed | Domain rule prevented the operation (e.g. finalizing an already-finalized subtask, moving a task into a workspace with no stages). |
422 Unprocessable Entity | Zod validation failed — see details. |
500 Internal Server Error | Unhandled 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. POSTshould always carry anIdempotency-Keyif you intend to retry. See Idempotency.- Transient
5xxresponses 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 Forbiddenis 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.