4000 0000 0000 0002 to force a decline. This page is the same
idea for Phosra: a flat table of concrete inputs that make the sandbox return a
specific outcome — the happy path, a forced deny, and every error status a real
integration has to handle (400, 404, 409, 429, and the tier-gated 403).
Every row was executed live against the sandbox while writing this page, and the
response bytes below are pasted verbatim. Nothing here is hypothetical.
Two neighbouring pages cover the values; this one covers the outcomes.
Sandbox test data is the flat lookup of every
fixed value (child IDs, provider DIDs, the 22 platforms). Test in the
sandbox is the guided tour. This page is the
error-and-branch trigger table — reach for it when you are writing the code that
handles a
404 or backs off a 429.Base URL
curl
below runs against $SANDBOX exactly as written.
There is no separate “magic value” namespace. Unlike a payments API, Phosra’s
triggers are real endpoint behaviours, not sentinel strings — a
404 comes from a
DID that genuinely is not configured, a 409 from a DID that genuinely is already on
the Trust List. That means every trigger here also matches production semantics: the
same input shape produces the same class of outcome against a live key. The only
sandbox-specific affordances are the seeded fixtures (the demo family, did:ocss:loopline)
and the self-serve reset levers, both documented below.The trigger table
Scan this, copy the row you need. Eachclass matches the Errors reference;
each links to the worked example below.
Provider connect — 200 vs the two 404s
GET /api/v1/providers/{did}/connect has three deterministic branches. This is the
single most useful pair to test against, because it lets you exercise both the
happy-path and not-found arms of your connect code without any auth.
200 — did:ocss:loopline
404 — did:ocss:courier (accredited, no connect config)
404 — unknown DID
The two
404s carry different messages on the same class. did:ocss:courier
is on the Trust List (accredited) but has no OAuth connect config — the
“real provider, not wired for connect” case. An unknown DID is a clean miss. Your
code should branch on the message, not just the status.Allow vs force-deny — the OAuth consent branch
The reference-provider consent screen is a real, server-rendered page. It is the sandbox’s allow/deny switch: thedecision query parameter deterministically
picks which redirect you get back, so you can drive both the granted and the
user-declined paths of your connect handler.

The live sandbox consent page — GET /oauth/authorize. Approving shares the seeded Mia, Leo, and Ava profiles; Deny returns error=access_denied.
302. The state you send is echoed back on both branches, so you can
verify your CSRF check on the deny path too. Custom mobile redirect schemes (e.g.
propagate://cb) are allowed — this is the same surface the
Propagate iOS parent flow drives.
The sandbox consent flow is stateless: the
sbxauth_… code and the sbxtok_…
token it exchanges for are opaque single-value strings, not persisted and not
PKCE-bound. Any freshly minted sbxauth_ code exchanges cleanly. This is a sandbox
affordance to keep tests short — a production IdP binds and expires both.Self-register — 409 and the two 400s
POST /api/v1/advisors/self-register is the fastest way to a deterministic 409:
send a DID that is already on the Trust List. The seeded did:ocss:loopline is always
present, so this reproduces with zero setup.
409
400s:
On the
-staging host, a 200 self-register additionally carries a
"warning" field steering you to the partner sandbox
(…-production.up.railway.app). The partner (-production) host — the base URL on
this page — returns no warning. The warning is keyed off X-Forwarded-Host, so it is
itself a deterministic, host-based trigger.POST /setup/quick — two clean 400s
setup/quick cleanly validates both a missing name and a malformed date — both are
caller errors, and both return 400, never a 500.
Treat
YYYY-MM-DD as a hard client-side requirement regardless — this 400 still fails
the call, it just fails it cleanly and tells you exactly what to fix. (Older deployments
that predate this validation may still return 500 Internal Server Error for a malformed
date; if you see that instead of the 400 above, you’re hitting a stale build.)401 — every signed endpoint without a signature
The write endpoints (/enforcement-endpoints, /harm-context,
/policies/{id}/rules, /platforms/{did}/endpoints, the sandbox consent-attestation
mint) require an RFC 9421 signature. Calling any of them with no
Signature header is a deterministic 401 — useful for asserting your client attaches
one before it ships.
401
429 — the rate limit is real
Every metered response — reads included — carries live rate-limit headers, and the
limit does trip. The product surface under /api/v1/* shares one window; a
GET /api/v1/platforms costs the same as a POST. Unmetered by design: /health, the
/.well-known/ocss/* discovery reads (Trust List, succession record), the Annex B
editions read (GET /api/v1/annex-b/editions/{n}), the sandbox /oauth/* consent
surface, and the signed census safety paths (enforcement endpoints, confirmations,
harm-context — deliberately never rate-gated, the fail-open invariant) — so hammer
those freely.
A healthy response, mid-window:
/api/v1/* calls from one caller) and the next call is a 429:
Retry-After (or X-RateLimit-Reset), sleeps, and retries
once — see the backoff recipe. A 429 is the only
error on this page worth retrying automatically; the 4xxs are deterministic and a
blind retry returns the same result.
The captured
x-ratelimit-reset (1783342740) is a Unix epoch from the moment of
capture; yours will differ. Assert on the header’s presence and the 429 status,
not the literal value.403 — the tier gate (signed requests only)
A 403 is the one outcome you cannot reach with a bare curl, and we call that
out rather than fake it: it means the request was cryptographically valid but not
authorized. The canonical trigger is a provisional-tier DID (what
self-register gives you) signing a rule write whose enforcement band its tier cannot
reach:
- Input: a valid RFC 9421 signature from a
provisionalDID, writing a Restricted-band rule toPOST /policies/{id}/rules. - Outcome:
403,class: standing_failure,failed_step: band_exceeds_tier.
consent:attestation standing produces the sibling
403 scope_failure. Both are fully specified — with the exact failed_step values —
in Errors › 403 standing and scope. To raise your
tier from provisional to verified/accredited (the tiers that clear the gate) is a
governance step, not an API call — see Production accreditation.
Reset & repeatability
The sandbox has no destructive reset button. Two levers keep your test runs clean:
Details and worked examples: Sandbox test data › Reset and idempotency.
Now build something
Errors reference
Every status, class, and
failed_step these triggers map to.Sandbox test data
The fixed values — child IDs, provider DIDs, the 22 platforms.
Back off a 429
The retry loop for the one error worth retrying.