Authentication

Two credential types, for two different places. Confusing them is the most common integration mistake and the one with the worst consequences, so the distinction is worth thirty seconds.

Client credentials Session token
Obtained from Partner console POST /tokens
Lives Your server, only A browser
Scope Your whole organisation One person, one session, one role
Lifetime Until rotated 15 minutes by default, 60 maximum
Authorises The REST API A rendered surface

Client credentials never reach a browser. They authorise everything your organisation can do. A session token is the browser-safe substitute: narrow, short-lived, and scoped to one person.


Client credentials

Issued once, on approval, through the partner console. The secret is shown exactly once and is not recoverable — store it in your secret manager before closing the dialog.

Client ID      psc_live_4f81c2a9
Client secret  pss_live_…            shown once

Getting an access token

POST /v1/oauth/token
Content-Type: application/json

{
  "client_id": "psc_live_4f81c2a9",
  "client_secret": "pss_live_…",
  "grant_type": "client_credentials"
}
{
  "access_token": "eyJhbGciOiJSUzI1NiIs…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "identity:write sessions:write content:read content:write generation:write exports:write"
}
GET /v1/patients
Authorization: Bearer eyJhbGciOiJSUzI1NiIs…

Access tokens live one hour. Cache and reuse one until it expires rather than minting per request — the token endpoint is rate-limited more tightly than the rest of the API, precisely to discourage that pattern.

The server SDK does all of this for you and you will never see an access token.

Scopes

Requested at approval and fixed on the credential. Ask for what you need; you can request a change later, and narrowing is instant while widening is reviewed.

Scope Grants
identity:read Read clinics, practitioners, patients, caregiver links
identity:write Create and update them
sessions:read Read sessions and summaries
sessions:write Create sessions, mint tokens and links, end and cancel
content:read Read playrooms, toolkits, games, storybooks, worksheets, forms
content:write Create and update content, attach it to playrooms, assign it
generation:write Every generation endpoint. Spends quota
artifacts:read Read and download artifacts
exports:write Create exports
notes:read Read clinical notes. Requires the notes capability on the organisation
changes:read Read the change feed

A call outside your granted scopes returns 403 forbidden with both the required and the granted lists in the problem document, so the fix is visible in the error rather than requiring a support conversation.

Rotation

Two credentials can be live at once, which is what makes zero-downtime rotation possible:

  1. Issue a second credential in the console. Both work.
  2. Deploy the new one.
  3. Confirm traffic has moved — the console shows last-used per credential.
  4. Revoke the old one.

Rotate on any suspicion of exposure, and on staff departure if your secret manager does not make that unnecessary.

Internet-protocol allowlisting

Optional and recommended. Restrict a credential to the addresses your backend calls from, and a leaked secret is useless off your network. Configured in the console as a list of address ranges; both IPv4 and IPv6 are supported.


Session tokens

Minted by your backend, consumed by a browser.

POST /v1/tokens
Authorization: Bearer <access token>
Idempotency-Key: 6f1c…
Content-Type: application/json

{
  "session": "sess_2Nk8pQvR7xLm",
  "subject": { "external_id": "staff_8842" },
  "role": "clinician",
  "origins": ["https://app.example-practice.com"],
  "ttl": "15m"
}
{
  "data": {
    "id": "tok_9Xm2Kd",
    "value": "eyJhbGciOiJFUzI1NiIs…",
    "role": "clinician",
    "surfaces": ["sandtray", "dollhouse", "whiteboard", "games"],
    "expires_at": "2026-09-02T15:15:00Z"
  }
}

The token endpoint you write

Twenty lines, and the only piece of this that must live on your backend.

// app/api/playspace/token/route.ts
export async function POST(request: Request) {
  const user = await yourAuth(request)               // your session, your rules
  const { sessionId } = await request.json()

  if (!(await userMayJoin(user, sessionId))) {
    return new Response('Forbidden', { status: 403 })
  }

  const token = await playspace.tokens.issue({
    session: sessionId,
    subject: { externalId: user.externalId },
    role: user.isClinician ? 'clinician' : 'patient',
    origins: [process.env.APP_ORIGIN],
    ttl: '15m',
  })

  return Response.json({ token: token.value, expiresAt: token.expiresAt })
}

Authorise before you mint. PlaySpace checks that the subject belongs to your organisation and that the session exists. It cannot check that this browser should be that subject — only you know that. The userMayJoin line is the whole security boundary.

Three gates at mint time

All three fail loudly rather than issuing a token that will not work:

Surfaces within the session's ceiling. Requesting more than the session permits returns 422 capability-missing.

Origins a browser will honour. Every origin is checked against what can validly appear in a frame-ancestors directive. A trailing slash, a path, a wildcard host or a plain hostname without a scheme is rejected at mint with 422 origin-not-allowed. This converts the single most frustrating failure in embedded software — a silently blank rectangle — into a message naming the offending value.

The organisation licensed for the surface. Otherwise 403 entitlement-denied, naming the surface.

Lifetime and renewal

Fifteen minutes by default, sixty maximum, and deliberately short: a token sitting in a tab a clinician left open over lunch should not still be live after lunch.

You do not write renewal. The React SDK calls your fetchToken again at eighty percent of the lifetime and swaps the new token in underneath a live surface without remounting it. If you are on the browser SDK directly, the same callback contract applies.

Revocation

DELETE /v1/tokens/tok_9Xm2Kd

Immediate. Call it when a clinician signs out of your application mid-session. Cancelling a session revokes every token issued against it.


Sandbox

Sandbox credentials are prefixed psc_sandbox_ and pss_sandbox_ and are issued self-serve — no agreement, no review. They are valid only against https://api.sandbox.playspace.health/v1 and are rejected by production, and production credentials are rejected by the sandbox. There is no configuration under which sandbox traffic can reach real clinical data.


What to do when something is refused

Status Type Cause Fix
401 unauthorized Missing, malformed or expired access token Re-fetch. Check you sent Bearer
403 forbidden Scope not granted Compare the two lists in the problem document
403 entitlement-denied Organisation not licensed for a surface Commercial, not technical
403 organisation-suspended Account suspended Contact partner engineering
422 capability-missing Token requests more than the session permits Widen the session or narrow the token
422 origin-not-allowed An origin no browser will honour The problem document names the value
429 rate-limited Too many requests Honour Retry-After

Log the request_id on every failure. Quoting one to partner engineering addresses a single request in our audit history and turns a day of diagnosis into a few minutes.