> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Limits & quotas

> The resource ceilings — page sizes, request-body byte caps, identifier lengths, and per-family counts — that are distinct from 429 rate-limit throttling. Every enforced value is sourced from a schema constraint or a live sandbox boundary probe.

This page covers the **resource ceilings** — how big a page you can pull, how large a
request body may be, how long an identifier can be. These are separate from
[rate limiting](/rate-limits): a rate limit throttles *how often* you may call
(`429 Too Many Requests`), while a resource limit bounds *how much* a single call may
carry or return (a `400`, a `413`, or a silently clamped page).

<Info>
  **Sourced, not guessed.** Every **enforced** value below is taken from a schema
  constraint or the census validation source. Two of them — the `did:ocss` slug bound
  (`400`) and a request-body cap (`413`) — were **reproduced live** against the partner
  sandbox at `https://phosra-api-sandbox-staging.up.railway.app` on **2026-07-06**;
  both real responses are pasted below verbatim. Ceilings the API does **not** currently
  enforce are labelled **Advisory** — do not design against them as hard guarantees.
</Info>

## Rate limit vs. resource limit

|            | **Rate limit**                                       | **Resource limit**                                          |
| ---------- | ---------------------------------------------------- | ----------------------------------------------------------- |
| Bounds     | requests **per 60 s window**                         | the size of **one** request/response                        |
| Signal     | `429` + `Retry-After` + `X-RateLimit-*`              | `400` (too long), `413` (body too large), or a clamped page |
| Fix        | back off and retry — see [Rate limits](/rate-limits) | shrink the payload / page and resend                        |
| Retryable? | yes, after the window resets                         | no — the same oversized input fails again                   |

<Note>
  A `413`/`400` from a resource limit is **not** retryable as-is. Retrying the identical
  oversized body just fails again. Split the work (smaller page, fewer items, shorter
  field) before you resend.
</Note>

## Pagination page sizes

List endpoints never *error* on `limit`, but they don't all behave the same way — some
enforce a hard ceiling, some accept anything. Read the column below before you assume a big
page works. See [Pagination](/pagination) for how to walk every page.

| Endpoint                                | `limit` default | `limit` bound (enforced)                                                             | `offset` |
| --------------------------------------- | --------------- | ------------------------------------------------------------------------------------ | -------- |
| `GET /api/v1/enforce/history`           | `20`            | **`100`** — values outside `1–100` fall **back to the `20` default**, *not* to `100` | ✅        |
| `GET /api/v1/webhooks/{id}/deliveries`  | `50`            | *(unbounded — advisory: keep ≤ 100)*                                                 | —        |
| `GET /api/v1/viewing-history/{childId}` | `50`            | *(unbounded — advisory: keep ≤ 100)*                                                 | ✅        |

<Warning>
  **The three list endpoints do not clamp the same way.** `enforce/history` accepts a `limit`
  only in `1–100`; ask for `101` (or `0`, or a non-number) and it silently uses the **`20`**
  default, so a too-big `limit` returns *fewer* rows, not the ceiling — always send a value
  ≤ 100. `webhooks/{id}/deliveries` and `viewing-history` accept **any positive** `limit`
  with **no** server bound; a page size above **100** is advisory only — it works today but
  isn't a guaranteed ceiling, and large pages compound your [rate-limit](/rate-limits) cost
  per call. Prefer `100` and page.
</Warning>

<Note>
  These page-size values are read from the request-handler and repository source
  (`browser_enforcement`, `webhook`, and `viewing_history`), not a live probe — the three
  list endpoints require an authenticated session, so they can't be reproduced with an
  anonymous `curl`.
</Note>

## Request-body size caps

There is **no single global body cap** — the census enforces size in **two layers**, and a
request must clear both:

1. **The outer signed-envelope cap — `10 MiB`.** Every RFC-9421-signed census write is buffered
   whole by the signature verifier (`Verifier.Require`) so the content digest can be checked over
   the exact bytes the handler will read. That buffer is bounded at **10 MiB** — the effective
   default cap on *every* signed endpoint, including the two every partner calls:
   `POST /api/v1/policies/{policyId}/rules` (rule-write) and `POST /api/v1/webhooks/inbound/{source}`
   (ingest). Exceed it and verification fails **before** the handler runs, with a `malformed`-class
   OCSS error (not a `413`).
2. **The inner per-handler cap.** Each handler then re-bounds its own body to a tighter, payload-sized
   limit. How the limit is enforced determines the failure signal:
   * **`http.MaxBytesReader`** endpoints fail **hard at the boundary** — a clean `413`
     (or, where the read is wrapped in a JSON decoder, a `400`).
   * **`io.LimitReader`** endpoints **silently truncate** at the limit, so the *validator* rejects the
     now-broken JSON with a `400`/`malformed` — you never see a `413`. Don't design against a `413`
     from these; watch the cap yourself.

Every value below is the exact `http.MaxBytesReader` / `io.LimitReader` constant in the census
source, paired with the exact **method + path** it guards.

### Outer envelope — all signed census writes

| Method + path                                                                                                                                            | Cap                    | Reader          | On exceed                                                    |
| -------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | --------------- | ------------------------------------------------------------ |
| `POST /api/v1/policies/{policyId}/rules` *(rule-write — the endpoint every partner calls)*                                                               | **10 MiB**             | verifier buffer | `malformed`-class error (`…exceeds the 10 MiB census bound`) |
| `POST /api/v1/webhooks/inbound/{source}` *(ingest)*                                                                                                      | **10 MiB**             | verifier buffer | `malformed`-class error                                      |
| **Any other RFC-9421-signed census write** *(alerts, harm-context, DSR, proposals, enforcement-confirmations, …) with no tighter inner cap listed below* | **10 MiB** *(default)* | verifier buffer | `malformed`-class error                                      |

<Note>
  The 10 MiB envelope is the ceiling, not a target — the largest real census payloads (bulk CSM
  reviews) sit far below it. It is enforced in `internal/ocsshttp/verifier.go` (`maxBodyBytes = 10 << 20`)
  and applies uniformly to everything behind `Verifier.Require`.
</Note>

### Inner per-handler caps

| Method + path                                                                                  | Inner cap                  | Reader                           | On exceed                                     |
| ---------------------------------------------------------------------------------------------- | -------------------------- | -------------------------------- | --------------------------------------------- |
| `POST /api/v1/subjects` *(signed; enforcement-agent only)*                                     | **1 MiB**                  | `MaxBytesReader` in JSON decoder | `400 invalid_json`                            |
| `POST /api/v1/compliance-bundles` *(signed; fan-out — N receipts)*                             | **512 KB**                 | `io.LimitReader`                 | truncates → decode fails (`400`/`malformed`)  |
| `POST /api/v1/routing-manifests` *(signed; fan-out)*                                           | **16 KiB**                 | `io.LimitReader`                 | truncates → decode fails                      |
| `POST /api/v1/minimization-attestation` *(signed)*                                             | **16 KiB**                 | `io.LimitReader`                 | truncates → strict decode fails (`malformed`) |
| `POST /api/v1/abuse-signal` *(signed)*                                                         | **8 KiB**                  | `io.LimitReader`                 | truncates → strict decode fails (`malformed`) |
| `POST /api/v1/receipts/{id}/appeal` *(redress; alias `…/alerts/{id}/appeal`)*                  | **64 KiB**                 | `MaxBytesReader`                 | `malformed`-class error                       |
| `POST /api/v1/advisors/{id}/payload-key` *(advisor payload-key declaration)*                   | **16 KiB**                 | `MaxBytesReader`                 | `413` (`…declaration size bound`)             |
| `POST /api/v1/accreditation/apply`                                                             | **32 KiB**                 | `MaxBytesReader`                 | `413` (`…accreditation-apply size bound`)     |
| `POST /api/v1/enclaves`                                                                        | **64 KiB**                 | `MaxBytesReader`                 | `413` (`…enclave registration size bound`)    |
| `POST /api/v1/webhooks/stripe`                                                                 | **64 KiB** (`65536` bytes) | `MaxBytesReader`                 | `413`                                         |
| `POST /api/v1/advisors/register` *(deprecated self-register shim — not signed)*                | **1 MiB**                  | `io.LimitReader`                 | truncates → `400` (no `413`)                  |
| `POST /api/v1/admin/advisors/{id}/record-attestation` *(admin-only)*                           | **16 KiB**                 | `MaxBytesReader`                 | `400 read_failed`                             |
| `POST /api/v1/sandbox/record-attestation` *(sandbox-only; F2 conformance loop)*                | **32 KiB**                 | `MaxBytesReader`                 | `413`                                         |
| `POST /classify` *(reference-enclave service — a **separate host**, not the `/api/v1` census)* | **1 MiB**                  | `MaxBytesReader`                 | `413`                                         |

<Note>
  **All other `/api/v1/...` write endpoints** fall back to the **10 MiB** outer envelope above — they
  set no tighter inner cap. These caps are generous for well-formed JSON: a rule bundle or attestation
  is kilobytes, not megabytes. If you are approaching a cap you are almost certainly batching too much
  into one call; split it and lean on [idempotency keys](/idempotency) so a retry is safe.
</Note>

### A real `413`, end to end

Post more than a `MaxBytesReader` cap and you get a `413` with a machine-readable body. This is
captured live from the partner sandbox — a **40 KiB** body against the 32 KiB `accreditation/apply`
cap:

```bash curl theme={null}
BASE=https://phosra-api-sandbox-production.up.railway.app

# Build a ~40 KiB JSON body — over the 32 KiB accreditation-apply cap
BIG=$(printf 'a%.0s' {1..40000})
printf '{"provider_did":"did:ocss:x","evidence":"%s"}' "$BIG" \
  | curl -s -X POST "$BASE/api/v1/accreditation/apply" \
      -H 'Content-Type: application/json' \
      --data-binary @- -w '\nHTTP %{http_code}\n'
```

```json Response · 413 theme={null}
{
  "error": "Request Entity Too Large",
  "message": "request body exceeds the accreditation-apply size bound",
  "code": 413
}
```

<Note>
  The `message` names the surface that rejected you (`…accreditation-apply size bound`), so
  a `413` tells you exactly which cap you hit. The cap fires **before** any field validation,
  so an oversized body never partially applies. Each endpoint's message is worded for its own
  cap (`…enclave registration size bound`, etc.).
</Note>

## Identifier & field lengths

| Field                                    | Max length    | Enforced by                      |
| ---------------------------------------- | ------------- | -------------------------------- |
| `did:ocss:<slug>` — the slug             | **128 bytes** | server validation (live-probed)  |
| `did:ocss:…#<kid>` — the key-id fragment | **128 bytes** | server validation                |
| Family / child / policy `name`           | **255 chars** | database column (`VARCHAR(255)`) |
| User `email` / `name`                    | **255 chars** | database column (`VARCHAR(255)`) |

The `name`/`email` bounds are the **database column width** (`VARCHAR(255)`) — an over-length
value is rejected at write time by Postgres, so it surfaces as a write failure rather than a
pre-validated `400`. The `did:ocss` slug bound, by contrast, is **live-enforced in the
handler** — a 200-byte slug is rejected before any lookup. Reproduce it:

```bash curl theme={null}
BASE=https://phosra-api-sandbox-production.up.railway.app
LONG=$(printf 'a%.0s' {1..200})
curl -s -X POST "$BASE/api/v1/advisors/register" \
  -H 'Content-Type: application/json' \
  -d "{\"did\":\"did:ocss:$LONG\",\"public_key_b64url\":\"x\"}"
```

```json Response · 400 theme={null}
{
  "error": "Bad Request",
  "message": "invalid_did: ocss: did:ocss slug is 200 bytes, exceeding the 128-byte bound",
  "code": 400
}
```

<Warning>
  The slug and key-id bounds are counted in **bytes, not characters**. A multi-byte UTF-8
  slug reaches the 128-byte ceiling in fewer than 128 visible characters. Keep DIDs ASCII
  and short.
</Warning>

## Per-account counts (advisory)

Phosra does **not** currently cap the number of children per family, webhooks per family,
or active API keys per org. These are **advisory** — plan for reasonable numbers, and do
not rely on the absence of a cap as a contract; a future release may introduce one.

| Resource                | Enforced ceiling  | Guidance                                                                                                                                   |
| ----------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Children per family     | *none (advisory)* | Real households are small; hundreds is a data-model smell, not a use case.                                                                 |
| Webhooks per family     | *none (advisory)* | One or two endpoints per family is typical. Fan out downstream, not at the webhook layer.                                                  |
| Active API keys per org | *none (advisory)* | Adding keys does **not** add quota — every key in an org [shares one rate-limit bucket](/rate-limits#the-limit). Rotate, don't accumulate. |

<Note>
  Because every key in an org shares **one** rate-limit window, minting more keys buys you
  no extra throughput — it only spreads the same 100 req/min across more callers. Need more
  headroom? Raise `rate_limit_rpm` on your plan, not your key count.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Rate limits" icon="gauge-high" href="/rate-limits">
    The per-window request quota, the `X-RateLimit-*` headers, and a copy-paste backoff loop.
  </Card>

  <Card title="Pagination" icon="list-ol" href="/pagination">
    Walk every page correctly with the `limit`/`offset` and cursor rules.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/errors">
    The full status/class table — where `400`, `413`, and `429` each sit.
  </Card>

  <Card title="Idempotency" icon="fingerprint" href="/idempotency">
    Make a retry after a clamp or split safe to replay.
  </Card>
</CardGroup>
