# Rate limits & errors

All control plane and gateway responses use the standard envelope from `@ozma_app/core`. Every response includes `meta.request_id` — cite it in support requests.

```json
{ "ok": false, "error": { "code": "insufficient_budget", "message": "…", "details": {} }, "meta": { "request_id": "req_01…" } }
```

Canonical enum: `packages/core/src/envelope.ts`.

## Rate limits

| Surface | Limit | Key |
|---------|-------|-----|
| Control plane (`api.ozma.app`) | ~**120** requests / 60s | Per API-key prefix or Cloudflare IP (KV bucket) |
| Gateway (`gateway.ozma.app`) | ~**300** requests / min | Per API-key prefix (`GATEWAY_RATE_LIMIT_PER_MIN`) |

Exceeded limits return **429** `rate_limited`. Login OTP and signup abuse paths have additional per-email/IP caps.

## Error code reference

| Code | HTTP | Surface | Meaning | Remediation |
|------|------|---------|---------|-------------|
| `unauthenticated` | 401 | Both | Missing, invalid, or expired auth | Provide valid `Bearer ozma_live_…` (gateway) or key/session (API) |
| `forbidden` | 403 | Both | Insufficient scope or policy block | Verify email for elevated scopes; check self-dealing; confirm org membership |
| `not_found` | 404 | Both | Resource or route missing | Confirm slug/id; check API is live not draft |
| `conflict` | 409 | API | Duplicate or state conflict | e.g. slug taken — choose another |
| `validation_failed` | 400 / 413 | Both | Invalid input or oversize body | Fix request schema; reduce body size (413 on gateway) |
| `insufficient_budget` | 402 | Gateway | Key lifetime budget exceeded | Raise `budget_cents` or use another key |
| `per_call_limit_exceeded` | 402 | Gateway | Call price > `per_call_max_cents` | Increase per-call max or cheaper endpoint |
| `daily_cap_exceeded` | 402 | Gateway | Daily cap exceeded (UTC day) | Raise `daily_cap_cents` or wait for UTC midnight reset |
| `rate_limited` | 429 | Both | Ozma rate limit | Exponential backoff; reduce request rate |
| `upstream_rate_limited` | 429 | Gateway | Provider returned HTTP 429 | Backoff; honor upstream `Retry-After` in details |
| `upstream_error` | 502 | Gateway | Provider error, timeout, bad JSON | Fix upstream path/params; inspect `details.status` |
| `payment_required` | 402 | Gateway / API | Suspended org or prepaid wallet empty | Pay invoice or add payment method at dashboard |
| `precondition_failed` | 412 | API | Wrong document/API state for action | e.g. activate only draft docs; edit only draft/live APIs |
| `terms_acceptance_required` | 428 | Gateway | Required provider terms not accepted | Session accept via `POST …/terms/accept` or `ozma terms accept` |
| `not_configured` | 503 | API | Optional subsystem unavailable | e.g. avatar upload without R2 — use `avatar_url` PATCH |
| `internal_error` | 500 / 503 | Both | Unexpected server or dependency failure | Retry; report `request_id`; SpendCounter outage → 503 on gateway |

## Gateway-specific notes

- **`daily_cap_exceeded` vs `insufficient_budget`:** both HTTP 402; read `error.code` — daily cap is calendar UTC, lifetime budget is cumulative on the key.
- **`terms_acceptance_required`:** includes `approval_url`, `snapshot_url`, `document_id` in `details`.
- **Idempotency replay:** success responses may set `meta.replayed: true`; not an error.

## Related

- [Troubleshooting guide](/docs/guides/troubleshooting)
- [Gateway reference](/docs/reference/gateway)
