@playspace/sdk
PlaySpace Partner Platform server SDK for TypeScript and JavaScript.
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
- Method reference — every resource, every method
- Type declarations — the precise contract
- Server SDK guide — narrative walkthrough
- API conventions — envelopes, pagination, idempotency, versioning
- OpenAPI specification
- Changelog
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.