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-production.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
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 onlimit, 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.
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:- 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) andPOST /api/v1/webhooks/inbound/{source}(ingest). Exceed it and verification fails before the handler runs, with amalformed-class OCSS error (not a413). - 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.MaxBytesReaderendpoints fail hard at the boundary — a clean413(or, where the read is wrapped in a JSON decoder, a400).io.LimitReaderendpoints silently truncate at the limit, so the validator rejects the now-broken JSON with a400/malformed— you never see a413. Don’t design against a413from these; watch the cap yourself.
http.MaxBytesReader / io.LimitReader constant in the census
source, paired with the exact method + path it guards.
Outer envelope — all signed census writes
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
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
Response · 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
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
Response · 400
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.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.