Skip to main content
Every error from the Driftless API (whether you reach it through the CLI, the MCP server, or directly) comes back as the same JSON envelope. An agent should branch on the machine-readable code, not parse the human message.

The envelope

{
  "statusCode": 403,
  "code": "MISSING_HUMAN_IDENTITY",
  "message": "Merging into Knowledge requires owner/admin authority: your own session or an owned owner/admin key (an agent acting on your explicit request). A faceless agent (ownerless key / OAuth) cannot merge.",
  "request_id": "a1b2c3d4",
  "retryable": false,
  "endpoint": "POST /api/v1/workspaces/acme/topics/auth-flow/approve"
}
FieldMeaning
statusCodeHTTP status.
codeStable, machine-readable error code (this list is additive-only).
messageHuman-readable, actionable. Tells you how to fix it. May change; don’t match on it.
request_idCorrelation id. Quote it when reporting an issue.
retryabletrue only for genuine transient failures (5xx). A 4xx is a definitive answer, so retrying won’t help.
endpointThe method + path that produced the error.
retryable is honest: the CLI auto-retries a GET once on a transient failure but never re-sends a write, and never retries a 4xx. If you build your own client, do the same.

Why a code at all

Input that’s wrong shouldn’t reach the agent as an opaque 500. Two layers guarantee that:
  1. Validation rejects a bad enum (status, visibility) with a field-level 400 before it touches the database.
  2. A Postgres-error translator catches any constraint violation that slips through and turns it into a coded 4xx, never a generic 500.
After those two, a 500 means what it should: a real bug or an outage. That’s also the only time retryable is true.

Code catalog

Validation & input

codeStatusWhen
VALIDATION_FAILED422 / 400A request body field failed validation (e.g. status must be one of: draft, proposed, reviewed, …).
BAD_REQUEST400A generic bad request with no more specific code.
INVALID_VALUE400A value violates a database CHECK constraint.
INVALID_ID400A path/body identifier is not a valid UUID.
REQUIRED_FIELD400A required field was missing.
FK_VIOLATION400References a record that does not exist.
SECRET_DETECTED400A topic field looked like a credential (API key, token, private key) and the write was blocked. Remove it, or set allow_secrets: true (CLI --allow-secrets) to override. The error masks the match; it never echoes the secret.

Access & identity

codeStatusWhen
UNAUTHORIZED401Missing or invalid API key / token. → driftless login --key <key>.
FORBIDDEN403Authenticated, but not allowed.
MISSING_HUMAN_IDENTITY403A faceless principal (an ownerless key, or an OAuth token with no owner/admin behind it) tried to merge into Knowledge. Re-issue the key from the dashboard so it carries your identity, or run the merge under an owner/admin.
INSUFFICIENT_ROLE403You lack the workspace role for this action: only an owner/admin can approve. Ask one to promote you in Settings → Members.
NOT_FOUND404The resource doesn’t exist (or another member’s private draft you can’t see, so we return 404, never 403).

Governance & conflicts

codeStatusWhen
DUPLICATE409A record with these values already exists.
DUPLICATE_TOPIC409A topic with that slug already exists in the workspace.
VERSION_CONFLICT409expected_version didn’t match: someone wrote since you read. Re-read and retry.
CONFLICT409A generic state conflict.
PROTECTED_RESOURCE403A protected/default resource can’t be modified or deleted.
RATE_LIMITED429Too many requests. Back off.

Server

codeStatusWhen
INTERNAL500An unexpected failure. retryable: true. Quote request_id if it persists.

Observability

Every captured error is also emitted to PostHog (api_error server-side, cli_command_error from the CLI) with code, status_code, and endpoint as properties, so the error surface is one queryable map, sliceable by type and endpoint rather than free-text.