# Pagination Errors

# Pagination and Errors

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

Every list tool (`artifacts.list`, `agents.list`, `sessions`, `webhooks.list`, `audit_log.list`, `knowledge_base.list`, `context_entries.list`, and so on) accepts the same base query parameters:

| Parameter | Type    | Default | Max   | Purpose             |
| --------- | ------- | ------- | ----- | ------------------- |
| `limit`   | integer | `50`    | `200` | Page size           |
| `offset`  | integer | `0`     | n/a   | Zero-indexed offset |

And returns the same envelope:

```json
{
  "items": [ ... ],
  "limit": 50,
  "offset": 0,
  "total": 147,
  "has_more": true
}
```

- `items`: array of rows for this page.
- `limit` / `offset`: the values actually applied (clamped if you overshot).
- `total`: total matching rows across all pages. Authoritative; safe for UI pagers.
- `has_more`: `true` if `offset + items.length < total`.

### Paging example

```bash
# First page
curl -s "$AMDAHL_BASE/artifacts?limit=50&offset=0" \
  -H "Authorization: Bearer $AMDAHL_KEY"

# Next page
curl -s "$AMDAHL_BASE/artifacts?limit=50&offset=50" \
  -H "Authorization: Bearer $AMDAHL_KEY"
```

A client-side pager can simply walk `offset` by `limit` while `has_more` is `true`.

## Filtering conventions

Most list tools expose a common set of filters:

| Parameter                           | Applies to                                       | Example                                |
| ----------------------------------- | ------------------------------------------------ | -------------------------------------- |
| `status`                            | Artifacts, agents, content sessions, webhooks    | `?status=draft`                        |
| `created_at_gte` / `created_at_lte` | Everything with a `created_at`                   | `?created_at_gte=2026-01-01T00:00:00Z` |
| `updated_at_gte` / `updated_at_lte` | Everything with an `updated_at`                  | `?updated_at_gte=2026-04-01T00:00:00Z` |
| `q`                                 | Search-capable lists (knowledge base, audit log) | `?q=churn`                             |

Tool-specific filters (for example `artifact_type` on artifacts, `profile` on agents, `event` on webhook deliveries) are documented on each tool's reference page under [api-reference/tools/](./api-reference/tools/_index.md).

## Sorting

Every list endpoint sorts by `created_at DESC` by default: newest first. Tools that need a different default override this in their reference documentation. To request an explicit sort, pass `order_by` and `order_dir`:

```
?order_by=updated_at&order_dir=asc
```

Fields you can sort on are listed per tool. Passing an unsupported field returns `invalid_input`.

## Error envelope

Every non-2xx response uses the same 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`.

## Standard error codes

| Code               | HTTP | When                                                                                        | Expected client reaction                                         |
| ------------------ | ---- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `unauthenticated`  | 401  | Missing or invalid credential                                                               | Reauthenticate, do not retry the exact call                      |
| `forbidden`        | 403  | Credential valid but lacks required scope or access to the resource                         | Check `details.missing_scope`, rotate with higher scopes or stop |
| `not_found`        | 404  | Target resource does not exist or is outside the caller's business                          | Treat as terminal; do not retry                                  |
| `invalid_input`    | 400  | Request body failed schema validation                                                       | Fix the payload; do not retry the same body                      |
| `invalid_argument` | 400  | Syntactically valid request whose semantics are wrong (bad sort field, out-of-range values) | Fix arguments; do not retry the same call                        |
| `conflict`         | 409  | State conflict (for example revoking a key already revoked, double-approving an outline)    | Refetch current state; reconcile                                 |
| `rate_limited`     | 429  | Rate limit exceeded                                                                         | Back off; see [rate-limits.md](./rate-limits.md)                 |
| `internal`         | 500  | Server error                                                                                | Retry with backoff for 5xx only                                  |

The full, continuously-generated catalogue of codes plus details keys lives at [api-reference/errors.md](./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 tool data.query.",
    "details": {
      "limit": 60,
      "window_seconds": 60,
      "retry_after_seconds": 12
    }
  }
}
```

## Idempotency

POST endpoints that create resources (`artifacts.create`, `context_entries.create`, `webhooks.create`, `agents.start`, `pages.create`) are not yet idempotent at the server level. A successful retry will create a duplicate row.

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

- For artifacts and context entries: include a caller-generated key in `metadata.client_request_id` and, before each retry, list with a filter on that key.
- For agents: poll `agent_run.list` with a stable tag before starting; if you find an active run, do not start a new one.

Tools that patch (`artifacts.update`), delete (`artifacts.archive`), or mark-complete are naturally idempotent and safe to retry.

## Reference

- Full error code catalogue with `details` shapes: [api-reference/errors.md](./api-reference/errors.md).
- Rate limit windows and headers: [rate-limits.md](./rate-limits.md).
- Per-tool filter and sort surfaces: [api-reference/tools/](./api-reference/tools/_index.md).
- Canonical data model shapes for list items: [api-reference/data-models.md](./api-reference/data-models.md).
