Skip to main content
Stripe gives you 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

No key, no signup. The sandbox is a full copy of the API seeded with the demo family (Mia, Leo, Ava) and the accredited reference providers. Every 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. Each class 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.
Verbatim responses:
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.
The reference-provider consent screen is a real, server-rendered page. It is the sandbox’s allow/deny switch: the decision 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.
Phosra sandbox OAuth consent page listing the seeded children Mia, Leo, and Ava with Approve and Deny buttons

The live sandbox consent page — GET /oauth/authorize. Approving shares the seeded Mia, Leo, and Ava profiles; Deny returns error=access_denied.

Both return 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
The two validation 400s:
Self-register is not idempotent — it is a create, not an upsert. A second call with the same DID is a 409, never a silent success. To register a fresh DID that returns 200 provisional, use a DID you have never registered (e.g. did:ocss:my-app-$(date +%s)) — see Test in the sandbox.
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:
Exhaust the window (> 100 /api/v1/* calls from one caller) and the next call is a 429:
The correct handler reads 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 provisional DID, writing a Restricted-band rule to POST /policies/{id}/rules.
  • Outcome: 403, class: standing_failure, failed_step: band_exceeds_tier.
A forged or mismatched 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.