Titentiten.devꦠꦶꦠꦺꦤ꧀
v0.1.1 · npmGitHub
DocsReferenceErrors

Errors

One error envelope, eleven codes, and a rule that an unauthorized record must never reveal that it exists.

11 codeserror + metax-request-id404 never 403

The envelope

Every failure has the same shape: an error object with a stable machine-readable code and a human-readable message, plus meta carrying at least the request id.

{ "error": { "code": "UNAUTHENTICATED", "message": "Credential is missing or invalid." },
  "meta": { "request_id": "req_f7144027d9d94061a2783ebd32df01e7" } }

Branch on code, never on message. Codes are part of the contract; messages are for the log, and several interpolate the offending value: Missing required scope "claims:write"., Resource is leased by another principal until 2026-07-30T11:02:14.000Z.

Request ids

meta.request_id is req_ + 32 lowercase hex, and the same value is returned in an x-request-id header on every response, success or failure. It is generated before routing, so even a 404 on an unknown path has one.

Every code

Code Status Raised when What a caller does
VALIDATION_ERROR 400 A field is missing, the wrong type, out of range, over a length limit, or the body is empty or not JSON. Also covers illegal state transitions: superseding a claim that is neither active nor disputed, revoking a superseded one, resolving a handoff addressed to someone else. Fix the request. Never retry unchanged. The message names the field or the current state.
UNRESOLVED_REFERENCE 400 An import references a project or observation that does not exist yet, or a write hits a foreign-key failure. Import in dependency order — projects, then observations, then claims. The message names the missing record type and up to three example ids.
UNAUTHENTICATED 401 No Authorization header, a non-Bearer scheme, a token that is not prefixed titen_sk_, an unknown key hash, or a revoked key. Check the credential. A 401 after it previously worked usually means the key was revoked.
FORBIDDEN 403 The credential is valid but lacks the route’s scope, tries to assert trust above its ceiling, tries to mint a key with authority it does not hold, or a federation peer is inactive, wrongly directed, or wrongly signed. Widen the scope or lower the trust deliberately. Not retryable.
NOT_FOUND 404 The path matches no route (Unknown endpoint.), or the resource does not exist, belongs to another organization, is private and not yours, or is an expired checkpoint. Treat as absent. Do not infer existence from a 404.
METHOD_NOT_ALLOWED 405 The path exists but not with that method. The message lists the allowed ones. Use a listed method.
CONFLICT 409 A unique constraint was violated, an Idempotency-Key was reused with a different body, a live lease is held by another principal, a workspace name is taken, a release is published or revoked from the wrong state, or an imported id already exists outside your organization. Read the message — most conflicts are a real state disagreement, not a transient race. Nothing partial was written.
PAYLOAD_TOO_LARGE 413 The declared content-length or the received body exceeds 1,048,576. Split the request. Export and import are paginated for this reason.
INTERNAL 500 An unexpected failure the service refuses to describe. Retry once, then report the request_id.
NOT_READY 503 GET /readyz found the storage check failing or the applied migration count not matching the expected one. Do not send traffic. The readiness body is in meta, so the reason is in the response.
UNAVAILABLE 503 Storage is unreachable, or the embedding model or vector index is unreachable during POST /v1/index/drain. Retryable. On a drain the queue is left untouched, so a later call picks up the same work.

Captured live

Three real responses from a running service. The 405 arrives without a credential: routing and method matching happen before authentication.

$ curl -i "$TITEN_URL/v1/keys"
HTTP/1.1 401 Unauthorized
{"error":{"code":"UNAUTHENTICATED","message":"Credential is missing or invalid."},
 "meta":{"request_id":"req_f7144027d9d94061a2783ebd32df01e7"}}
$ curl "$TITEN_URL/nope"
{"error":{"code":"NOT_FOUND","message":"Unknown endpoint."},
 "meta":{"request_id":"req_3895034103df464c90749a01d54bfb09"}}
$ curl -X GET "$TITEN_URL/v1/observations"
{"error":{"code":"METHOD_NOT_ALLOWED","message":"Allowed methods: POST."},
 "meta":{"request_id":"req_1440258be9684a36822415b8a5827987"}}

401, 403 or 404

401 — we do not know who you are. The credential is missing, malformed, unknown or revoked.

403 — we know who you are and this capability is not yours. Scope, trust ceiling, or a peer/webhook in a state that forbids the operation.

404 — we will not say. The record may not exist, may belong to another organization, or may exist and be invisible to you. All three answer identically.

WHY A FOREIGN ID IS NEVER 403

A 403 on a record id confirms that the id is real. Distinguishing “no such claim” from “that claim is not yours” hands an attacker an oracle for enumerating another tenant’s identifiers. Every cross-scope denial funnels through one indistinguishable 404, and the Atlas view compiler applies the same rule to graph topology, so a claim or observation you may not see contributes no node, edge or label to the projection.

GET /v1/claims/:id/evidence applies the same rule: evidence you may not read is reported as a hidden_source_count, never as content.

Codes that carry extra meta

Two errors put non-sensitive detail in meta alongside the request id.

DETAIL IN META

NOT_READY the whole readiness body: ready, runtime, revision, schema with applied and expected counts, checks, capabilities.

UNAVAILABLE on a drain: pending (how many outbox rows are still queued) and retryable: true.

When the failure is not one Titen raised

Anything that is not a recognised error becomes 500 INTERNAL with the fixed message “Request could not be completed.” Only the error’s name is logged, next to the request id, never its message: a raw database message can carry SQL fragments or observation content.

Three storage failures are recognised on the way out and translated:

Underlying failure Becomes Why not something else
FOREIGN KEY constraint failed 400 UNRESOLVED_REFERENCE A missing dependency, not a conflict. Saying “conflicted with an existing record” about a record that does not exist sends the operator looking for the wrong thing.
UNIQUE / other constraint failure 409 CONFLICT The write lost a race with an existing row. Every write is one atomic batch, so nothing partial survives.
no such table, unable to open database, readonly database 503 UNAVAILABLE The deployment is broken, not the request. Retrying the same request later can succeed.

Next

↑↓ navigate↵ open