Gipo
Conventions

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, or the organization is not on the Pro plan.
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. Also returned when a subtask estimate or due date change needs confirmation (the body carries code and parent), or when a task estimate or due date conflicts with its subtasks. See Subtasks.
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.
429 Too Many RequestsYour organization exceeded its per-minute request ceiling, or had too many requests in flight at once. The Retry-After header carries the seconds to wait. See Rate limits.
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

  • A 429 is not a rejection of your request — wait the number of seconds in Retry-After and send it again. Retries inside the same per-minute window are refused too.
  • 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}).
  • API access is a Pro feature. When the key's organization is not on Pro, every request returns 403 with a billing field: { "error": "API access is available on the Pro plan.", "billing": { "kind": "capability", "key": "api" } }. The key is kept and works again once the organization upgrades.
  • 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