Skip to main content
A platform (also called a provider) is anything Phosra can push a child-safety policy to — a DNS filter, a router, an app, an operating system. Before Phosra can enforce anything, a parent has to connect the platform and approve sharing their children’s profiles. This guide walks the full connect ceremony — discovery → consent → token → profiles — against the public sandbox. Every request and response below is verbatim live output, captured while writing this page. No API key, nothing to install. The four calls chain end to end: discover the endpoints (GET /providers/{did}/connect) → send the parent to consent (authorize_url) → exchange the returned code for a token (token_url) → read the approved profiles (profiles_url). Each step below carries a Fields & errors reference you can expand.
Sandbox-first. The reference provider used here — did:ocss:loopline — is seeded into the partner sandbox at https://phosra-api-sandbox-production.up.railway.app. Nothing you connect there touches a real family. The endpoint shapes are identical in production; you swap the base URL for https://prodapi.phosra.com and connect a real provider DID.

Before you start

Set the base URL once so every step is copy-paste:
export PHOSRA_BASE="https://phosra-api-sandbox-production.up.railway.app"
You will connect the sandbox’s reference provider, Loopline (did:ocss:loopline). It is a real, accredited entry on the sandbox Trust List with a live OAuth surface — the same shape a production provider exposes.
1

Discover the provider's connect endpoints

Every connectable provider publishes its OAuth endpoints at GET /providers/{did}/connect. Start there — never hard-code the URLs.
curl -s "$PHOSRA_BASE/api/v1/providers/did:ocss:loopline/connect"
Real response (200):
{
  "authorize_url": "https://phosra-api-sandbox-production.up.railway.app/oauth/authorize",
  "token_url": "https://phosra-api-sandbox-production.up.railway.app/oauth/token",
  "profiles_url": "https://phosra-api-sandbox-production.up.railway.app/oauth/profiles",
  "scopes": ["child_profiles.read"],
  "name": "Loopline"
}
A provider that is accredited but has not configured a connect surface returns 404 provider connect config not available. In the sandbox, did:ocss:courier is deliberately left unconfigured so you can test that branch.
Path parameter
FieldTypeRequiredDescription
didstringyesThe provider’s decentralized identifier, e.g. did:ocss:loopline. URL-encode the : characters if your client does not do it for you.
Response fields (200)
FieldTypeDescription
authorize_urlstring (URL)Where you send the parent’s browser to grant consent (step 2).
token_urlstring (URL)Where you exchange the authorization code for a token (step 4).
profiles_urlstring (URL)Where you read the approved child profiles with the token.
scopesstring[]The OAuth scopes the provider will request. Loopline requests child_profiles.read — read-only access to the approved children.
namestringHuman-readable provider name, safe to show a parent.
Errors
StatusmessageWhen it happens
404provider connect config not availableThe DID is accredited but has no connect surface configured (e.g. did:ocss:courier). Do not retry — the provider must publish endpoints first.
404provider not foundThe DID is not on the Trust List at all. Check the value against GET /.well-known/ocss/trust-list.
2

Send the parent to the consent page

Redirect the parent’s browser to authorize_url with your client_id (the provider DID), a redirect_uri, and an opaque state you generate:
GET /oauth/authorize
      ?client_id=did:ocss:loopline
      &redirect_uri=https://loopline.example/callback
      &state=xyz789
      &response_type=code
The sandbox serves a real consent screen. The parent sees exactly which child profiles they are about to share, and chooses Approve or Deny:
Phosra sandbox consent page titled 'Connect your family to this app?' listing Mia, Leo, and Ava as child profiles, with Approve and Deny buttons
Query parameters
FieldTypeRequiredDescription
client_idstringyesThe provider DID you are connecting, e.g. did:ocss:loopline.
redirect_uristring (URL)yesWhere Phosra sends the parent back after they decide. Custom schemes (e.g. myapp://callback) are accepted for native apps.
statestringyesAn opaque value you generate. It is echoed back unchanged — compare it on return to defend against CSRF.
response_typestringyesAlways code for this flow.
Outcomes
ResultWhat Phosra returns
Parent approves302 redirect to redirect_uri?code=…&state=… (step 3).
Parent denies302 redirect to redirect_uri?error=access_denied&state=…no code. Handle this branch; see Disconnect & reconnect.
Errors
StatusWhen it happens
400redirect_uri is missing or malformed. Phosra will not redirect to an absent callback, so it fails closed with a 400 instead.
3

Receive the authorization code

On Approve, Phosra redirects back to your redirect_uri with a short-lived code and the state you sent (verify it matches before continuing):
302 Location: https://loopline.example/callback?code=sbxauth_2hg1obaYzTs5XiK80A-NnuVhzOHfktze&state=xyz789
On Deny, no code is issued — you get ?error=access_denied&state=… instead. Handle both. See Disconnect & reconnect for the decline path in full.
Query parameters Phosra appends to your redirect_uri
FieldTypePresent whenDescription
codestringapprovedShort-lived authorization code (sbxauth_… in the sandbox). Exchange it once at token_url.
statestringalwaysThe exact state you sent. Reject the response if it does not match the value you generated.
errorstringdeniedaccess_denied when the parent declined. No code is present — do not attempt the token exchange.
4

Exchange the code for an access token

Trade the code for a bearer token at token_url:
curl -s -X POST "$PHOSRA_BASE/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "sbxauth_2hg1obaYzTs5XiK80A-NnuVhzOHfktze",
    "client_id": "did:ocss:loopline",
    "redirect_uri": "https://loopline.example/callback"
  }'
Real response (200):
{
  "access_token": "sbxtok_ymSQQBw4aPijjuX8xuU5R6ASjrDSXXC6",
  "expires_in": 3600,
  "scope": "profiles",
  "token_type": "Bearer"
}
On the scope value. Discovery advertises the requested scope as child_profiles.read (the canonical name), while the issued token echoes the short form scope: "profiles". Both name the same grant — read-only access to the approved children. Key your logic off the token you were issued, not off a hard-coded string, and treat the two as equivalent.
Request body (application/json)
FieldTypeRequiredDescription
grant_typestringyesAlways authorization_code for this flow.
codestringyesThe authorization code from the callback.
client_idstringyesThe provider DID — must match the one you sent to authorize.
redirect_uristringyesMust match the redirect_uri used at authorize.
Response fields (200)
FieldTypeDescription
access_tokenstringBearer token (sbxtok_…) for GET /oauth/profiles.
expires_inintegerSeconds until the token expires (3600 = 1 hour).
scopestringThe granted scope, returned as profiles — equivalent to the child_profiles.read scope from discovery (see note above).
token_typestringAlways Bearer. Send it as Authorization: Bearer <access_token>.
Errors
StatuserrorWhen it happens
400unsupported_grant_typegrant_type is missing or not authorization_code.
400invalid_requestThe body is malformed or a required field is absent.
Sandbox stub behaviour. The sandbox /oauth/token surface is a stateless reference stub: it does not persist or validate the code, so an expired or reused code still returns a token in the sandbox. In production, a provider’s real token endpoint returns 400 invalid_grant for an expired, reused, or unknown code — write your error handling for that before you go live.
5

Read the shared child profiles

Call profiles_url with the token. You get back exactly the children the parent approved — each with a stable subject_ref you use for policy and enforcement calls:
curl -s "$PHOSRA_BASE/oauth/profiles" \
  -H "Authorization: Bearer sbxtok_ymSQQBw4aPijjuX8xuU5R6ASjrDSXXC6"
Real response (200):
[
  { "id": "mia", "displayName": "Mia", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a1", "kind": "child" },
  { "id": "leo", "displayName": "Leo", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a2", "kind": "child" },
  { "id": "ava", "displayName": "Ava", "subject_ref": "a11ce0fa-0000-4000-8000-0000000000a3", "kind": "child" }
]
The platform is now connected. Hold onto each subject_ref — that is the handle you pass to the policy and enforcement endpoints in Set up a family & kids.
Request header
HeaderRequiredDescription
AuthorizationyesBearer <access_token> from the token step.
Response fields (200) — an array, one object per approved child
FieldTypeDescription
idstringProvider-local child id (e.g. mia).
displayNamestringChild’s display name, safe to show a parent.
subject_refstring (UUID)Stable cross-system handle. Pass this to the policy and enforcement endpoints — it does not change across reconnects.
kindstringSubject kind, child for these entries.
Errors
StatuserrorWhen it happens
401invalid_tokenNo Authorization header was sent.
Sandbox stub behaviour. Because the sandbox surface is stateless, any non-empty Bearer value returns the seeded profiles — only a missing header yields 401. A production provider validates the token and returns 401 invalid_token for any expired or forged token, so treat 401 as “re-run the connect ceremony” in your client.

Which platform supports which rule?

Not every platform can enforce every rule. Discovery endpoints let you pick the right one before you ask a parent to connect:
# Every platform that can do DNS-level web filtering
curl -s "$PHOSRA_BASE/api/v1/platforms/by-capability?capability=web_filtering"
# → [ "fire_tablet", "apple", "microsoft", "cleanbrowsing", "controld", "nextdns" ]
Check each platform’s enforcement_mode: dns, device, and oauth2 platforms are applied programmatically, while manual_attested platforms require the parent to complete a guided step by hand. See Platforms & enforcement modes.

Production notes

In production, connecting an account-linked platform (one where you hold a per-family credential rather than an OAuth grant) uses POST /compliance with your phosra_live_… key — see First-time setup. Those endpoints are family-scoped and require an authenticated caller who is a member of the family, so they cannot be exercised anonymously against the open sandbox. The OAuth ceremony above is the anonymous, fully runnable path.

Next steps

Set up a family & kids

Build a family, add children, and get an age-appropriate policy in one call.

Disconnect & reconnect

Handle the decline path, tear a link down, and re-approve cleanly.