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-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 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.
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

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
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.
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.