Skip to main content

Documentation Index

Fetch the complete documentation index at: https://archie.com/docs/llms.txt

Use this file to discover all available pages before exploring further.

The REST API uses RFC 9457 Problem Details for every error. Every error response has Content-Type: application/problem+json and a consistent JSON shape.

Error response format

{
  "type": "/errors/unique-constraint",
  "title": "Conflict",
  "status": 409,
  "detail": "Unique constraint \"students_email_key\" violated. Key (email)=(alice@example.com) already exists.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "unique", "message": "The value for email already exists" }
  ]
}
FieldTypeWhat it tells you
typeStringA stable, machine-readable URI identifying the error category. Switch on this in your code.
titleStringA short human-readable summary, paired with the HTTP status.
statusNumberThe HTTP status code, duplicated in the body for convenience.
detailStringA human-readable explanation specific to this occurrence.
instanceStringThe request path that produced the error.
errorsArrayField-level details when applicable (validation, constraints).

Status code reference

4xx — client errors

StatusTypeMeaning
400/errors/bad-requestMalformed request.
400/errors/missing-project-idMissing X-Project-ID header.
400/errors/complexity-limitQuery exceeded depth or relation limits.
401/errors/unauthorizedMissing or invalid auth token.
403/errors/forbiddenAuthenticated, but not allowed.
404/errors/item-not-foundRecord does not exist.
404/errors/not-foundResource not found (composite PK, etc.).
404/errors/table-not-foundTable doesn’t exist in the schema.
405/errors/view-is-readonlyTried to write to a view.
409/errors/unique-constraintDuplicate value on a unique field.
422/errors/validation-failedBody or parameters fail validation.
422/errors/foreign-key-constraintReferenced record doesn’t exist.
422/errors/not-null-constraintRequired field is missing or null.
422/errors/check-constraintField value violates a check constraint.
429/errors/rate-limit-exceededToo many requests.

5xx — server errors

StatusTypeMeaning
500/errors/internal-errorUnexpected server error.
504/errors/query-timeoutDatabase query exceeded time limit.

Common errors

Validation (422)

Required fields missing or wrong types.
{
  "type": "/errors/validation-failed",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more fields failed validation",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "required", "message": "The field 'email' is required" },
    { "field": "age", "rule": "type", "message": "Expected number, got string" }
  ]
}

Unique constraint (409)

Duplicate value on a unique column. The errors[].field tells you which column.
{
  "type": "/errors/unique-constraint",
  "title": "Conflict",
  "status": 409,
  "detail": "Unique constraint \"students_email_key\" violated. Key (email)=(alice@example.com) already exists.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "email", "rule": "unique", "message": "The value for email already exists" }
  ]
}
For handling this without the read-then-write race condition, use a GraphQL upsert.

Foreign key (422)

Referencing a related record that doesn’t exist.
{
  "type": "/errors/foreign-key-constraint",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Foreign key constraint violated. Referenced record does not exist.",
  "instance": "/api/rest/students",
  "errors": [
    { "field": "cityId", "rule": "foreign_key", "message": "Referenced record in 'cities' does not exist" }
  ]
}

Not found (404)

{
  "type": "/errors/item-not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Record not found",
  "instance": "/api/rest/students/00000000-0000-0000-0000-000000000000"
}

Rate limit exceeded (429)

{
  "type": "/errors/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Try again in 30 seconds.",
  "instance": "/api/rest/students"
}
The response includes rate-limit headers — back off using Retry-After:
HeaderWhat it tells you
Retry-AfterSeconds to wait before retrying.
X-RateLimit-LimitRequests allowed per window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetSeconds until the window resets.

Query timeout (504)

{
  "type": "/errors/query-timeout",
  "title": "Gateway Timeout",
  "status": 504,
  "detail": "Database query exceeded the maximum execution time",
  "instance": "/api/rest/students"
}
Tighten the filter, paginate, add an index, or move to a view that pre-computes the result.

Field names in errors

Database column names are converted to camelCase in error messages — a constraint on created_at reports the field as createdAt. This matches what you send and receive in JSON bodies.

Handling errors in code

Switch on status first, then on type for ambiguous statuses:
StatusWhat to do
400Fix the request. Don’t retry.
401Refresh the auth token. Retry once.
403Surface a “no permission” message. Don’t retry — same user, same outcome.
404Treat as the resource not existing.
409Show the user a “duplicate” message. For automatable cases, switch to upsert.
422Surface field-level errors back to the user. Don’t retry the same payload.
429Honor Retry-After with exponential backoff.
5xxRetry with backoff. If persistent, alert.
async function call(url, init) {
  const res = await fetch(url, init);
  if (!res.ok) {
    const problem = await res.json();
    switch (problem.type) {
      case "/errors/unique-constraint":
        throw new DuplicateError(problem.errors?.[0]?.field);
      case "/errors/rate-limit-exceeded":
        const wait = parseInt(res.headers.get("Retry-After") ?? "1", 10);
        await new Promise((r) => setTimeout(r, wait * 1000));
        return call(url, init);
      default:
        throw new ApiError(problem);
    }
  }
  return res.json();
}

FAQ

400 means the request itself was malformed (bad JSON, wrong content type). 422 means the request was syntactically fine but semantically invalid — required fields missing, types wrong, constraints violated. The distinction tells your code whether to retry the same payload or fix the request shape first.
401 means “we don’t know who you are” — the auth token is missing, expired, or invalid. 403 means “we know who you are, but you can’t do this” — the role doesn’t have the necessary permission. Refresh tokens for 401; surface a permission error for 403.
Yes, with exponential backoff. Most 5xx errors are transient. Cap retries at 3–5 and alert if they keep failing.
The errors array gives field-level reasons. For complex cases, the instance URL points to the request that failed — useful for support tickets.
2xx and 4xx responses are cached and returned on retry with the same key. 5xx responses are not cached, so retries actually re-run the operation. See Idempotency.