@playspace/sdk

PlaySpace Partner Platform server SDK for TypeScript and JavaScript.

npm types

Bring therapeutic play into your platform: shared sandtrays, dollhouses, whiteboards and games, plus generated storybooks, worksheets, forms and three-dimensional models — attributed to your clinicians and clients, and exportable on demand.

npm install @playspace/sdk

Node 20 and above. Also runs on Deno, Bun, and the Vercel and Cloudflare edge runtimes.


Quick start

import { PlaySpace } from '@playspace/sdk'

const playspace = new PlaySpace()   // reads PLAYSPACE_CLIENT_ID / _SECRET / _ENV

// 1. Tell us who is who. Keyed on your identifier; safe to call repeatedly.
const clinician = await playspace.practitioners.upsert({
  externalId: 'staff_8842',
  clinic: { externalId: 'location_northgate' },
  firstName: 'Dana',
  lastName: 'Okafor',
})

// 2. Open a session against an appointment that lives in your scheduler.
const session = await playspace.sessions.create({
  appointment: { externalId: 'appt_11923' },
  clinician: { externalId: 'staff_8842' },
  participants: [{ patient: { externalId: 'client_55130' } }],
  playroom: 'child-default',
  surfaces: ['sandtray', 'dollhouse', 'whiteboard', 'games'],
})

// 3. Mint a token for the person at the screen.
const token = await playspace.tokens.issue({
  session: session.id,
  subject: { externalId: 'staff_8842' },
  role: 'clinician',
  origins: ['https://app.example-practice.com'],
})

Hand token.value to @playspace/react and a working shared sandtray renders in your application. The token carries the role, so components need no role parameter and cannot be handed the wrong one.


Configuration

const playspace = new PlaySpace({
  clientId: process.env.PLAYSPACE_CLIENT_ID,
  clientSecret: process.env.PLAYSPACE_CLIENT_SECRET,
  environment: 'production',   // or 'sandbox'
  timeout: 30_000,
  maxRetries: 3,
})
Variable Purpose
PLAYSPACE_CLIENT_ID Client identifier
PLAYSPACE_CLIENT_SECRET Client secret. Server-side only
PLAYSPACE_ENV production or sandbox. Defaults to production

These are server credentials. They authorise everything your organisation can do. Never put them in a browser bundle, a mobile application, or anything you ship to a client. The browser-safe substitute is a session token from tokens.issue().


What the client handles for you

Access tokens. Fetched, cached and refreshed before expiry. You never see one.

Idempotency. Every mutation sends a key derived from its arguments, so a retried network failure cannot create two sessions or start two generation jobs. Override with { idempotencyKey }.

Retries. Transient failures and rate limits retry with exponential backoff and jitter, honouring Retry-After. Mutations retry only where the idempotency key makes it safe.

Pagination. Every list is an async iterator that pages transparently.

for await (const patient of playspace.patients.list({ clinic: clinic.id })) {
  // pages fetched as needed; break whenever you like
}

const page = await playspace.patients.list({ limit: 50 }).page()
const everything = await playspace.patients.list().all()

Types. Generated from the same OpenAPI contract that serves the API. A field in the types is a field on the wire.


Your identifiers, everywhere

You are the record of truth for people. PlaySpace holds a pointer.

await playspace.patients.get('pt_9Kd2mXwF')
await playspace.patients.get({ externalId: 'client_55130' })   // equivalent

Every method accepts your identifier in place of ours, and every object echoes yours back. If adding a column for a PlaySpace identifier is inconvenient, do not add one.

upsert creates on first sight and updates thereafter, so it is safe on every synchronisation pass. There is no reconciliation story because there is nothing to reconcile.


Generating content

Everything expensive is a job. Nothing blocks.

const job = await playspace.storybooks.generate({
  clinician: { externalId: 'staff_8842' },
  subject:   { externalId: 'client_55130' },
  prompt: 'A story about starting at a new school.',
  pages: 8,
  characters: [
    { name: 'Nia', role: 'protagonist', description: 'seven, box braids, yellow raincoat' },
  ],
})

const storybook = await job.wait({ timeout: '5m' })
const pdf = await playspace.storybooks.download(storybook.id, { format: 'pdf' })

clinician is required on every generation call because generation costs money and somebody owns that. subject is optional and is what makes an artifact personal rather than library content.

Check your allowance before a bulk run:

const usage = await playspace.usage.get()
// { period: '2026-09', resources: { storybooks: { used: 214, limit: 1000 }, ... } }

Errors

import { QuotaExceededError, ValidationError, RateLimitError } from '@playspace/sdk'

try {
  await playspace.storybooks.generate({ ... })
} catch (error) {
  if (error instanceof QuotaExceededError) return scheduleRetryAfter(error.resetsAt)
  if (error instanceof ValidationError)    return reportToClinician(error.fields)
  if (error instanceof RateLimitError)     return retryAfter(error.retryAfter)
  throw error
}

Every error carries type, status and requestId. Log requestId — quoting one to partner engineering addresses a single request in our audit history. Full catalogue: error reference.


Testing

import { PlaySpaceMock } from '@playspace/sdk/testing'

const playspace = new PlaySpaceMock({ seed: 'deterministic' })

const job = await playspace.storybooks.generate({ ... })
const book = await job.wait()   // resolves instantly from a fixture

PlaySpaceMock implements the whole surface in memory with no network. Generation resolves against fixtures rather than a model, so a suite is fast, deterministic and free.

Against the real sandbox, playspace.messages.list() returns every email and text message the platform would have sent, so you can assert on delivery without a mailbox. The sandbox never sends to a real address.


Documentation


Compatibility

Semantic versioning, independent of the API version. 1.x targets /v1 and will keep working against it for the life of that API version.

The SDK ignores what it does not recognise: an unknown enumerated value or response field is preserved rather than thrown on, so a server-side addition cannot break a deployed integration.


Support

Partner engineering: partner-engineering@playspace.health, or the shared channel opened with your sandbox key. Include a requestId and the failure is usually diagnosed the same day.