Skip to main content
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: 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).
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.

Rate limit vs. resource limit

Rate limitResource limit
Boundsrequests per 60 s windowthe size of one request/response
Signal429 + Retry-After + X-RateLimit-*400 (too long), 413 (body too large), or a clamped page
Fixback off and retry — see Rate limitsshrink the payload / page and resend
Retryable?yes, after the window resetsno — the same oversized input fails again
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.

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 for how to walk every page.
Endpointlimit defaultlimit bound (enforced)offset
GET /api/v1/enforce/history20100 — values outside 1–100 fall back to the 20 default, not to 100
GET /api/v1/webhooks/{id}/deliveries50(unbounded — advisory: keep ≤ 100)
GET /api/v1/viewing-history/{childId}50(unbounded — advisory: keep ≤ 100)
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 cost per call. Prefer 100 and page.
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.

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 + pathCapReaderOn exceed
POST /api/v1/policies/{policyId}/rules (rule-write — the endpoint every partner calls)10 MiBverifier buffermalformed-class error (…exceeds the 10 MiB census bound)
POST /api/v1/webhooks/inbound/{source} (ingest)10 MiBverifier buffermalformed-class error
Any other RFC-9421-signed census write (alerts, harm-context, DSR, proposals, enforcement-confirmations, …) with no tighter inner cap listed below10 MiB (default)verifier buffermalformed-class error
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.

Inner per-handler caps

Method + pathInner capReaderOn exceed
POST /api/v1/subjects (signed; enforcement-agent only)1 MiBMaxBytesReader in JSON decoder400 invalid_json
POST /api/v1/compliance-bundles (signed; fan-out — N receipts)512 KBio.LimitReadertruncates → decode fails (400/malformed)
POST /api/v1/routing-manifests (signed; fan-out)16 KiBio.LimitReadertruncates → decode fails
POST /api/v1/minimization-attestation (signed)16 KiBio.LimitReadertruncates → strict decode fails (malformed)
POST /api/v1/abuse-signal (signed)8 KiBio.LimitReadertruncates → strict decode fails (malformed)
POST /api/v1/receipts/{id}/appeal (redress; alias …/alerts/{id}/appeal)64 KiBMaxBytesReadermalformed-class error
POST /api/v1/advisors/{id}/payload-key (advisor payload-key declaration)16 KiBMaxBytesReader413 (…declaration size bound)
POST /api/v1/accreditation/apply32 KiBMaxBytesReader413 (…accreditation-apply size bound)
POST /api/v1/enclaves64 KiBMaxBytesReader413 (…enclave registration size bound)
POST /api/v1/webhooks/stripe64 KiB (65536 bytes)MaxBytesReader413
POST /api/v1/advisors/register (deprecated self-register shim — not signed)1 MiBio.LimitReadertruncates → 400 (no 413)
POST /api/v1/admin/advisors/{id}/record-attestation (admin-only)16 KiBMaxBytesReader400 read_failed
POST /api/v1/sandbox/record-attestation (sandbox-only; F2 conformance loop)32 KiBMaxBytesReader413
POST /classify (reference-enclave service — a separate host, not the /api/v1 census)1 MiBMaxBytesReader413
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 so a retry is safe.

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:
curl
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'
Response · 413
{
  "error": "Request Entity Too Large",
  "message": "request body exceeds the accreditation-apply size bound",
  "code": 413
}
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.).

Identifier & field lengths

FieldMax lengthEnforced by
did:ocss:<slug> — the slug128 bytesserver validation (live-probed)
did:ocss:…#<kid> — the key-id fragment128 bytesserver validation
Family / child / policy name255 charsdatabase column (VARCHAR(255))
User email / name255 charsdatabase 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:
curl
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\"}"
Response · 400
{
  "error": "Bad Request",
  "message": "invalid_did: ocss: did:ocss slug is 200 bytes, exceeding the 128-byte bound",
  "code": 400
}
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.

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.
ResourceEnforced ceilingGuidance
Children per familynone (advisory)Real households are small; hundreds is a data-model smell, not a use case.
Webhooks per familynone (advisory)One or two endpoints per family is typical. Fan out downstream, not at the webhook layer.
Active API keys per orgnone (advisory)Adding keys does not add quota — every key in an org shares one rate-limit bucket. Rotate, don’t accumulate.
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.

Next steps

Rate limits

The per-window request quota, the X-RateLimit-* headers, and a copy-paste backoff loop.

Pagination

Walk every page correctly with the limit/offset and cursor rules.

Errors

The full status/class table — where 400, 413, and 429 each sit.

Idempotency

Make a retry after a clamp or split safe to replay.