# Errors, pagination and limits

URL: https://docs.spacerun.co/docs/platform/guides/errors-pagination-limits

> Error body shapes, list pagination, permissions, rate limits and maintenance responses.

## Error bodies

Two shapes exist. Branch on status first.

**Application errors** (auth, permissions, not found, conflicts, ...):

```json
{ "error": "concurrent_limit" }
```

`error` is a machine code; `details` is optional and is omitted when undefined. It can be structured data or a string. Some operations can return both application and validation errors with HTTP 400; inspect the body as well as the status.

**Request validation errors** from `zod` validators on body, query, params and headers use a different shape, with status `400`:

```json
{
  "success": false,
  "error": {
    "name": "ZodError",
    "issues": [
      { "code": "too_small", "minimum": 8, "type": "string", "inclusive": true, "path": ["idempotency-key"], "message": "String must contain at least 8 character(s)" }
    ]
  }
}
```

Other cases:

| Status | Body |
| --- | --- |
| 401 | `{"error":"missing_token" \| "invalid_token" \| "invalid_audience"}` |
| 403 | `{"error":"forbidden","details":{"perm":"..."}}` |
| 404 (unknown route) | `{"error":"not_found"}` |
| 429 | `{"error":"rate_limited"}` |
| 500 | `{"error":"internal"}` (details are only in server logs; quote `x-request-id`) |

Device-code endpoints under `/v1/public/device` return OAuth-style `{"error":"..."}` bodies instead.

## Pagination

Several list endpoints (for example admin users and store lists) take:

| Query | Default | Notes |
| --- | --- | --- |
| `q` | none | Free-text filter where supported. |
| `page` | `1` | 1-based. |
| `per_page` | `25` | Maximum `100`. |
| `sort` | endpoint-specific | `field:asc` or `field:desc`. |

```json
{ "data": [], "meta": { "page": 1, "per_page": 25, "total": 0 } }
```

List shapes differ by operation:

| Style | Example | Important difference |
| --- | --- | --- |
| Page and total | Staff users/store lists | `meta` includes total. |
| Page without total | User credit ledger | `meta` only has page/per_page. |
| Limit and offset | Storage browsing | Metadata includes prefix/limit/offset. |
| Cursor and limit | Runtime session lists | Follow the operation's cursor envelope. |
| Bare array | Public products | No data/meta wrapper. |

Do not use a universal pagination parser. Check each operation in the [reference](https://docs.spacerun.co/docs/platform/reference).

## Rate limits

Fixed-window limits, counted per user (or client IP when unauthenticated). Exceeding one returns `429 rate_limited`.

| Endpoint | Limit | Window | Keyed by |
| --- | --- | --- | --- |
| `POST /v1/public/device/code` | 3 | 60 s | IP |
| `POST /v1/public/device/token` | 30 | 60 s | IP (returns `400 slow_down`) |
| `POST /v1/me/device/approve` | 5 | 60 s | user |
| `POST /v1/me/support` group | 5 | 1 h | user |
| Waitlist signup (`/v1/public`) | 5 | 60 s | IP |
| Email unsubscribe (`/v1/public`) | 30 | 60 s | IP |

Device polling also enforces its own `interval` (`slow_down` includes the required `interval`).

## Maintenance mode

Operators can put the platform in `degraded` or `lockdown` mode. Clients then see:

```http
HTTP/1.1 503 Service Unavailable
Retry-After: 30

{ "error": "panic_mode", "level": "lockdown", "reason": "..." }
```

- `degraded`: new checkouts and `POST /v1/me/sessions` are refused; everything else works.
- `lockdown`: all writes from public and `/v1/me` clients are refused.
- Reads, `POST /v1/me/sessions/stop`, `/health` and staff routes stay available.

Retry after the `Retry-After` delay. This is distinct from `503 unavailable` on session start, which means the runtime failed to create the session.
