Docs

Pagination and errors

Page through list responses with cursors, and decode the error envelope every endpoint returns

Every list endpoint on the Amdahl Platform API follows the same shape, and every error response follows the same envelope. Code once against the conventions on this page and you will not have to special-case individual tools.

List endpoint conventions

The public list endpoints — GET /chats, GET /routines, GET /agents — share the same paging parameters:

ParameterTypeDefaultMaxPurpose
limitinteger50200Page size
offsetinteger0n/aZero-indexed offset

And the same envelope shape: a named collection array plus the paging facts (values are echoed back clamped if you overshot), inside the data envelope every successful response on this surface carries:

json
{
  "data": {
    "chats": [ ... ],
    "total": 147,
    "limit": 50,
    "offset": 0
  }
}
  • The collection key matches the resource: chats on /chats, routines on /routines, agents on /agents.
  • total: total matching rows across all pages. Authoritative; safe for UI pagers.
  • There is no has_more field — derive it client-side: offset + rows.length < total.
  • GET /agents returns the merged roster (Amdahl library + workspace agents) without paging — it is bounded small by construction.

Paging example

bash
# First page
curl -s "$AMDAHL_BASE/chats?limit=50&offset=0" \
  -H "X-API-Key: $AMDAHL_KEY"

# Next page
curl -s "$AMDAHL_BASE/chats?limit=50&offset=50" \
  -H "X-API-Key: $AMDAHL_KEY"

A client-side pager can simply walk offset by limit while offset + rows.length < total.

Filtering and sorting

Filters are per-endpoint, not a generic vocabulary:

EndpointFilters
GET /chatsstatus (e.g. ?status=complete)
GET /routinesenabled (e.g. ?enabled=true)

Ordering is fixed per endpoint — Chats list newest activity first, Routines newest first — there is no order_by parameter on the public surface. An unsupported query parameter is ignored or returns invalid_argument, never silently reinterpreted.

Error envelope

Most failures use one JSON envelope:

json
{
  "error": {
    "code": "invalid_input",
    "message": "Human readable description of what went wrong.",
    "details": {
      "field": "scopes",
      "issue": "must be a non-empty array"
    }
  }
}
  • code: a machine-readable string from the catalogue below.
  • message: a short human-readable description. Safe to surface in UIs.
  • details: optional object with structured context. Shape depends on code.

Clients should branch on code, never on the exact text of message. HTTP status is included but is a coarser signal than code.

Three shapes, not one — check the status before you read error

error is not always an object, so body.error.code is undefined rather than a thrown exception on two of the three shapes. That failure is silent: an error handler written against the envelope above will quietly fail to recognise an expired key. Branch on HTTP status first.

1. Operation errors — 4xx, inside the data envelope. A request that reached an operation and was refused by it. success is false and the typed error sits beside it:

json
{
  "data": {
    "success": false,
    "error": {
      "code": "invalid_argument",
      "message": "Unknown filter field \"not_a_field\" on surface \"deals\". Known fields: deal_id, …",
      "details": { "field": "not_a_field", "surface": "deals" }
    }
  }
}

2. Schema-validation errors — 400, bare. The body never reached an operation, so there is no data envelope. This is the shape at the top of this section.

3. Authentication and routing failures — 401 / 404, bare, error is a STRING. These short-circuit ahead of the typed error handler, so they carry no code and no details:

json
{
  "error": "Unauthorized",
  "message": "Valid OAuth token or API key required. Send an API key as \"Authorization: Bearer <key>\" or \"X-API-Key: <key>\"; send an OAuth token as \"Authorization: Bearer <token>\"."
}

A 404 is the same shape with no message. Treat 401 and 404 as terminal from the status alone; the codes in the table below (unauthenticated, not_found) describe the intent of those statuses, not a string you will find in the body.

Standard error codes

CodeHTTPWhenExpected client reaction
unauthenticated401Missing or invalid credentialReauthenticate, do not retry the exact call
forbidden403Credential valid but lacks required scope or access to the resourceCheck details.missing_scope, rotate with higher scopes or stop
not_found404Target resource does not exist or is outside the caller's businessTreat as terminal; do not retry
invalid_input400Request body failed schema validationFix the payload; do not retry the same body
invalid_argument400Syntactically valid request whose semantics are wrong (bad sort field, out-of-range values)Fix arguments; do not retry the same call
conflict409State conflict (for example revoking a key already revoked, double-approving an outline)Refetch current state; reconcile
rate_limited429Rate limit exceededBack off; see rate-limits.md
internal500Server errorRetry with backoff for 5xx only

The full, continuously-generated catalogue of codes plus details keys lives at api-reference/errors.md.

Retry strategy

  • Retry: internal (5xx) and rate_limited (429). Use exponential backoff with jitter. Start at 500ms, double each attempt, cap at 30 seconds, give up after 5 attempts unless your workload can tolerate more.
  • Do not retry: every other 4xx. invalid_input, forbidden, not_found, and conflict all mean "this exact call will not succeed". Retrying wastes quota and can trigger rate_limited on top of the original failure.
  • Honor Retry-After. When present on a 429 or 503 response (in seconds), wait that long before the next attempt.

Example rate_limited body:

json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded for POST /search/query.",
    "details": {
      "limit": 60,
      "window_seconds": 60,
      "retry_after_seconds": 12
    }
  }
}

Idempotency

POST endpoints that create resources (POST /chat, POST /routines, POST /agents) are not yet idempotent at the server level. A successful-but-timed-out first attempt plus a retry can create two records (two Chats, two routines).

Until a server-side Idempotency-Key header lands (tracked in changelog.md as a planned feature), deduplicate on the client:

  • For Chats: name the Chat deterministically and list with GET /chats before retrying a START; if the named Chat already has a fresh run, poll it instead of starting another.
  • For agents: pass an explicit slug on create — a retry that raced a success returns a typed slug_conflict instead of creating a duplicate, which makes create effectively idempotent per slug.
  • For routines: list GET /routines and match on name before re-creating.

PATCH updates, DELETEs, resume, and cancel are naturally idempotent and safe to retry — a repeat resume against a run that already resumed returns invalid_state, not a double-apply.

Reference