Quickstart

A working shared sandtray, from nothing, in five steps. Everything below runs against the sandbox, which is seeded with synthetic clinicians, clients and appointments and needs no agreement to use.


1. Install

npm install @playspace/sdk @playspace/react
# Server credentials. Never ship these to a browser.
PLAYSPACE_CLIENT_ID=psc_sandbox_4f81c2a9
PLAYSPACE_CLIENT_SECRET=pss_sandbox_...
PLAYSPACE_ENV=sandbox

2. Introduce your people

One call each. You pass your identifier; we return ours and remember the correspondence. Calling it again with the same externalId updates rather than duplicates, so this is safe to run on every sync.

import { PlaySpace } from '@playspace/sdk'

const playspace = new PlaySpace()  // reads the environment variables above

const clinic = await playspace.clinics.upsert({
  externalId: 'location_northgate',
  name: 'Northgate Family Practice',
  country: 'CA',
})

const clinician = await playspace.practitioners.upsert({
  externalId: 'staff_8842',
  clinic: clinic.id,
  firstName: 'Dana',
  lastName: 'Okafor',
  email: 'dana.okafor@example-practice.com',
  role: 'member',
})

const client = await playspace.patients.upsert({
  externalId: 'client_55130',
  clinic: clinic.id,
  firstName: 'A.',
  lastName: 'R.',
  dateOfBirth: '2018-04-11',
  caregivers: [{ externalId: 'client_55129', relationship: 'parent' }],
})

On the example above. caregivers links a dependent to the adult who holds consent and receives every link the platform sends. For a paediatric caseload this is not optional metadata — it determines who can be emailed, who can join, and who can request an export.

We ask for the minimum that makes the product work. A first initial and a last initial are a complete and acceptable name. Nothing here needs to be more than what a clinician needs to recognise the right person in a list.


3. Open a session

Against an appointment that lives in your scheduler. We do not need the appointment itself — only your identifier for it, so that everything the session produces can be traced back to it later.

const session = await playspace.sessions.create({
  appointment: { externalId: 'appt_11923', scheduledAt: '2026-09-02T15:00:00Z' },
  clinician: clinician.id,
  participants: [{ patient: client.id }],
  playroom: 'child-default',
  surfaces: ['sandtray', 'dollhouse', 'whiteboard', 'games'],
})

console.log(session.id)  // sess_2Nk8pQvR7xLm

surfaces is the ceiling for this session. A surface not listed here cannot be opened by anyone in it, no matter what the front end asks for.


4. Mint a token for the person at the screen

This is the one piece that must live on your backend. Your application authenticates its own user, then asks us for a token scoped to that user's role in that session.

// 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()

  const token = await playspace.tokens.issue({
    session: sessionId,
    subject: { externalId: user.externalId },
    role: user.isClinician ? 'clinician' : 'patient',
    origins: ['https://app.example-practice.com'],
    ttl: '15m',
  })

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

Tokens are short-lived on purpose. You do not need to handle expiry yourself — step 5 shows the provider refreshing itself by calling this same endpoint again.


5. Render

'use client'
import { PlaySpaceProvider, Sandtray } from '@playspace/react'

export function PlayPanel({ sessionId }: { sessionId: string }) {
  const fetchToken = async () => {
    const res = await fetch('/api/playspace/token', {
      method: 'POST',
      body: JSON.stringify({ sessionId }),
    })
    return (await res.json()).token
  }

  return (
    <PlaySpaceProvider fetchToken={fetchToken}>
      <Sandtray />
    </PlaySpaceProvider>
  )
}

That is the whole client integration.

fetchToken is called once on mount and again whenever the current token is close to expiring. A clinician can leave this panel open all day and it will not strand them mid-session — which is exactly why the token lifetime is fifteen minutes rather than eight hours.


What you just built

Open that panel as a clinician in one browser and as a client in another, and both people are in the same tray. Figures move under both cursors. The tray autosaves. When the session ends, the saved state, the duration, and every artifact produced in it are attributable to the appointment identifier you passed in step 3.

You wrote roughly sixty lines and did not implement a single piece of real-time synchronisation.


Where to go next

Render more than one surface. Swap <Sandtray /> for <Playroom /> and the clinician gets the full environment — the sandtray, the dollhouse, the whiteboard, the game library, and the shelf of activities — with a navigation rail they steer and the client follows. See the React SDK.

Generate something for this client. A storybook about the thing they are working on, ready before the next appointment. See Content and generation.

Learn what happened. Poll the change feed for artifacts, session outcomes and note availability. See Core concepts.

Let them take it with them. One call packages everything a client has ever made into a downloadable bundle. See Portability and exports.


Sandbox notes

  • The sandbox is seeded with three clinics, twelve clinicians and roughly two hundred synthetic clients on realistic caseloads.
  • Generation runs against the same models as production but is capped at one hundred jobs per day per sandbox organisation. Results are real; they are just rate-limited.
  • Sandbox data is reset every Sunday. Do not build anything that assumes an identifier survives the week.
  • The sandbox will not send an email or a text message to any address, ever. Outbound messages are captured and readable through playspace.messages.list() so you can assert on them in tests.