@playspace/sdk — method reference

Every resource and every method. Precise signatures are in index.d.ts; this page is the annotated version.

Conventions used throughout:

  • IdOrRef is a PlaySpace identifier string, or { id }, or { externalId }. All three are accepted anywhere an identifier is.
  • Paginated<T> is an async iterator. for await to page transparently, .page() for one page, .all() to collect.
  • Every mutation accepts a trailing { idempotencyKey }. Omit it and the SDK derives one from the arguments.
  • Every method may throw one of the typed errors.

Client

new PlaySpace(options?: PlaySpaceOptions)
Option Default Notes
clientId PLAYSPACE_CLIENT_ID
clientSecret PLAYSPACE_CLIENT_SECRET Server-side only
environment PLAYSPACE_ENV, then production production or sandbox
baseUrl derived Rarely needed
timeout 30000 Per request, milliseconds
maxRetries 3 Mutations retry only where idempotency makes it safe
headers Added to every request
fetch global Supply your own
playspace.request<T>(method, path, init?)

Escape hatch for an endpoint this SDK version does not model yet. Handles authentication, retries and idempotency; returns the unwrapped data.


organisation

playspace.organisation.get(): Promise<Organisation>

Your organisation, its status, and its enabled capabilities.

playspace.organisation.surfaces(): Promise<SurfaceAvailability[]>

Which surfaces this organisation is licensed for, and which of those can render in a partner-hosted frame. Render your own entry points from this list — a surface absent here is refused at token mint with entitlement-denied, and a clinician should never click something that cannot work.

playspace.organisation.groups(options?): Paginated<PracticeGroup>

The optional hierarchy level above the clinic, for multi-location practices.


clinics · practitioners · patients

The three identity resources share a shape.

playspace.clinics.list(options?): Paginated<Clinic>
playspace.clinics.upsert(body, options?): Promise<Clinic>
playspace.clinics.get(id): Promise<Clinic>
playspace.clinics.update(id, body, options?): Promise<Clinic>
playspace.clinics.archive(id, options?): Promise<void>

upsert is the write verb you want. Keyed on externalId, it creates on first sight and updates thereafter, so it is safe on every synchronisation pass. update is a partial write for when you do not have the full record to hand and throws NotFoundError for an unknown identifier.

archive is soft. The record becomes unreadable rather than being destroyed. Archiving a clinic with active practitioners or patients throws ValidationError.

List filters

Resource Filters
clinics group, includeArchived
practitioners clinic, includeArchived
patients clinic, practitioner, includeArchived

Archived records are excluded unless includeArchived is set.

patients.search

playspace.patients.search({ query, clinic?, practitioner?, limit? }): Promise<Patient[]>

A POST under the hood, and there is no GET equivalent. A person's name in a query string is captured by platform request logs before anything at the application layer can act on it, so the term travels in a body. This is a routing decision, not a redaction one.

patients.caregivers

playspace.patients.caregivers(id): Promise<CaregiverLink[]>

caregivers

playspace.caregivers.link(body, options?): Promise<CaregiverLink>
playspace.caregivers.unlink(linkId, options?): Promise<void>
await playspace.caregivers.link({
  patient:   { externalId: 'client_55130' },
  caregiver: { externalId: 'client_55129' },
  relationship: 'parent',
  receivesSessionLinks: true,
  canRequestExport: true,
})

Load-bearing, not decorative. The link decides who receives a session link, who may join alongside a child, and who may request an export. canRequestExport is checked by exports.create and is why it exists here rather than being assumed.


sessions

playspace.sessions.create(body, options?): Promise<Session>

Created against an appointment that lives in your scheduler. PlaySpace needs your identifier for it, not the appointment itself.

surfaces is the ceiling for the session: a surface absent from it cannot be opened by anyone, whatever a token or front end asks for. Omit it for every licensed surface.

The response carries links.clinician and links.patient — hosted, role-scoped, and never stored.

playspace.sessions.list(options?): Paginated<Session>
playspace.sessions.get(id): Promise<Session>
playspace.sessions.update(id, body, options?): Promise<Session>
playspace.sessions.end(id, options?): Promise<Session>
playspace.sessions.cancel(id, body?, options?): Promise<Session>
playspace.sessions.summary(id): Promise<SessionSummary>
playspace.sessions.links(id, options?): Promise<SessionLinks>

list filters on status, clinician, patient, scheduledAfter, scheduledBefore.

get omits links — they are ephemeral. Call links() when you are about to put them in front of somebody.

cancel revokes every outstanding token and link for the session.

summary

const summary = await playspace.sessions.summary(sessionId)

Duration, participants, surfaces used, artifacts produced, games played.

Three caveats before you build a report from gamesPlayed. Only the clinician's play bracket is persisted, because a participant on a session token holds no account identity. Every duration is an upper bound — a backgrounded tab keeps accruing. And a remount can split one continuous play into two entries, so sum by game rather than assuming one entry per play.


tokens

playspace.tokens.issue(body, options?): Promise<SessionToken>
playspace.tokens.revoke(tokenId, options?): Promise<void>
const token = await playspace.tokens.issue({
  session: 'sess_2Nk8pQvR7xLm',
  subject: { externalId: 'staff_8842' },
  role: 'clinician',
  surfaces: ['sandtray', 'whiteboard'],   // optional; inherits the session's list
  origins: ['https://app.example-practice.com'],
  ttl: '15m',                              // maximum '60m'
})

Three gates apply at mint, and all three fail loudly rather than issuing a token that will not work: requested surfaces within the session's ceiling, origins a browser will honour as frame sources, and the organisation licensed for each surface.

Authorise before you mint. PlaySpace verifies the subject belongs to your organisation and the session exists. It cannot verify that this browser should be that subject — only you know that.

revoke is immediate. Call it when a clinician signs out of your application mid-session.


playrooms · toolkits

playspace.playrooms.list(options?): Paginated<Playroom>
playspace.playrooms.create(body, options?): Promise<Playroom>
playspace.playrooms.get(id): Promise<Playroom>
playspace.playrooms.update(id, body, options?): Promise<Playroom>
playspace.playrooms.archive(id, options?): Promise<void>
playspace.playrooms.attach(id, content, options?): Promise<PlayroomContent>
playspace.playrooms.detach(id, contentId, options?): Promise<void>
const playroom = await playspace.playrooms.create({
  title: 'Anxiety — ages 6 to 9',
  type: 'child',
  items: ['sandTray', 'dollhouse', 'whiteboard', 'multiplayerGames', 'activityShelf'],
  contents: [
    { type: 'storybook', id: 'sb_7Hn3xQ' },
    { type: 'worksheet', id: 'ws_3Bn8kR' },
    { type: 'game', slug: 'worry-pet' },
  ],
})

items selects which live surfaces the environment offers. contents is the ordered library attached to it.

toolkits exposes list, create, get, update and archive with the same content model and no environment settings.


games

playspace.games.list(options?): Paginated<Game>
playspace.games.get(slug): Promise<Game>

The catalog this organisation is licensed for, across every provider.

for await (const game of playspace.games.list({
  skills: ['emotional-regulation'],
  players: 2,
  ageMin: 6,
  ageMax: 10,
})) { ... }

Filters: skills, players, ageMin, ageMax, category, provider.


storybooks

playspace.storybooks.generate(body, options?): Promise<Job<Storybook>>

Asynchronous. Two to four minutes for eight pages.

const job = await playspace.storybooks.generate({
  clinician: { externalId: 'staff_8842' },
  subject:   { externalId: 'client_55130' },
  session:   { externalId: 'appt_11923' },
  prompt: 'A story about starting at a new school.',
  pages: 8,
  ageGroup: '5-8',
  readingLevel: 'grade-2',
  style: 'watercolour',
  characters: [
    { name: 'Nia', role: 'protagonist', description: 'seven, box braids, yellow raincoat' },
    { name: 'Pip', role: 'supporting',  description: 'a small grey cat with one white ear' },
  ],
})

Naming characters locks their appearance across pages. The description is carried into every page's image generation, so a character looks the same on page eight as on page one. Omit characters and they are inferred from the prompt, then locked after the first page.

playspace.storybooks.list(options?): Paginated<Storybook>
playspace.storybooks.get(id): Promise<Storybook>
playspace.storybooks.update(id, { title?, published? }, options?): Promise<Storybook>
playspace.storybooks.archive(id, options?): Promise<void>
playspace.storybooks.download(id, { format }): Promise<DownloadLink>

playspace.storybooks.pages.list(storybookId): Promise<StorybookPage[]>
playspace.storybooks.pages.updateText(pageId, text, options?): Promise<StorybookPage>
playspace.storybooks.pages.regenerate(pageId, { prompt? }, options?): Promise<Job<StorybookPage>>

playspace.storybooks.assign(id, { subject, notifyCaregiver? }, options?): Promise<Artifact>

assign creates a personal copy that tracks reading position, so a child can read between appointments and the clinician can see how far they got.

Download formats: pdf, epub, png per page, json.


worksheets

playspace.worksheets.generate(body, options?): Promise<Job<Worksheet>>
playspace.worksheets.list(options?): Paginated<Worksheet>
playspace.worksheets.create(body, options?): Promise<Worksheet>
playspace.worksheets.get(id): Promise<Worksheet>
playspace.worksheets.update(id, body, options?): Promise<Worksheet>
playspace.worksheets.archive(id, options?): Promise<void>
playspace.worksheets.assign(id, { subject, dueAt?, notifyCaregiver? }, options?): Promise<Artifact>
playspace.worksheets.copies.list(options?): Paginated<WorksheetCopy>
playspace.worksheets.download(id, { format }): Promise<DownloadLink>

A completed copy is its own artifact, distinct from the blank it came from. The blank is library content with no subject; the copy has one. copies.list filters on subject, worksheet and status.


forms

playspace.forms.generate({ clinician, prompt, maxFields?, scored? }, options?): Promise<Job<Form>>
playspace.forms.extract({ clinician, file, filename }, options?): Promise<Job<Form>>

generate builds a form from a description. extract reads an existing paper intake sheet — portable document, PNG or JPEG, up to 20 megabytes — and produces a structured, fillable form from it. The latter is the most common request from clinics with twenty years of paper.

playspace.forms.list(options?): Paginated<Form>
playspace.forms.create(body, options?): Promise<Form>
playspace.forms.get(id): Promise<Form>
playspace.forms.update(id, body, options?): Promise<Form>
playspace.forms.archive(id, options?): Promise<void>
playspace.forms.assign(id, { subject, routeToCaregiver?, dueAt? }, options?): Promise<Artifact>

playspace.forms.responses.list(options?): Paginated<FormResponse>
playspace.forms.responses.get(responseId): Promise<FormResponse>

Set routeToCaregiver for a paediatric client so the completion link reaches the adult who holds consent rather than the child.

Responses come back as structured answers with a computed score for scored instruments — never a rendered blob.


studio · models

playspace.studio.generate({ clinician, subject?, prompt, players?, duration? }, options?)
  : Promise<Job<GeneratedGame>>
playspace.studio.games.list(options?): Paginated<GeneratedGame>

Four to eight minutes. Produces a real playable application, launchable in a session like any catalog game and attachable to a playroom.

playspace.models.generate(body, options?): Promise<Job<Model>>
playspace.models.list(options?): Paginated<Model>
// From a description
await playspace.models.generate({
  clinician: { externalId: 'staff_8842' },
  source: 'text',
  prompt: 'a small brown terrier with a red collar, sitting',
})

// From a photograph
await playspace.models.generate({
  clinician: { externalId: 'staff_8842' },
  source: 'image',
  file: await fs.readFile('./dog.jpg'),
  filename: 'dog.jpg',
})

Three to six minutes, producing a GLB usable in the sandtray immediately.

An uploaded photograph is discarded after generation. Not retained, not used for training, never in an export. The capability exists for objects, pets and places — do not upload an image containing a person.


jobs

playspace.jobs.list(options?): Paginated<Job>
playspace.jobs.get(jobId): Promise<Job>
playspace.jobs.cancel(jobId, options?): Promise<Job>

Every generation call returns a Job with three methods of its own:

job.wait(options?): Promise<T>        // polls until terminal
job.refresh(): Promise<Job<T>>        // re-read current state
job.cancel(): Promise<Job<T>>

Pick by where your code runs.

// A script: block.
const result = await job.wait({ timeout: '5m' })

// A request handler: poll and return.
const current = await playspace.jobs.get(job.id)

// A worker: the change feed, and never poll a job identifier again.
await playspace.changes.subscribe({ onChange: handle })

wait throws JobFailedError on failure and TimeoutError if the timeout elapses — the work continues either way.

A failed job does not consume quota, so a retry after a failure is free. job.reason is written for a clinician rather than an engineer.

Typical durations: storybook two to four minutes, worksheet one to two, form under thirty seconds, generated game four to eight, model three to six.


artifacts

playspace.artifacts.list(options?): Paginated<Artifact>
playspace.artifacts.get(artifactId): Promise<Artifact>
playspace.artifacts.download(artifactId, { format }): Promise<DownloadLink>

The single index over everything PlaySpace produced and kept, queryable from any end of the attribution triple.

await playspace.artifacts.list({ subject:   { externalId: 'client_55130' } })
await playspace.artifacts.list({ createdBy: { externalId: 'staff_8842' }, type: 'storybook' })
await playspace.artifacts.list({ session:   { externalId: 'appt_11923' } })

Filters: subject, createdBy, session, type, createdAfter.

json is available for every artifact type and is always complete — no rendered format carries information its structured form omits. Download links are short-lived and single-use; request one when you are ready to stream it.


exports

playspace.exports.create(body, options?): Promise<Export>
playspace.exports.list(options?): Paginated<Export>
playspace.exports.get(exportId): Promise<Export>
playspace.exports.download(exportId): Promise<ExportDownload>
const exp = await playspace.exports.create({
  subject: { externalId: 'client_55130' },
  include: ['storybooks', 'worksheets', 'formResponses', 'sandtraySaves', 'sessionSummaries'],
  format: 'bundle',
  requestedBy: { externalId: 'staff_8842' },
  reason: 'clientRequest',
})

const { parts, expiresAt } = await exp.wait({ timeout: '30m' })

requestedBy must resolve to somebody with standing. A clinician for their own clients, an administrator for any client in their clinic, a caregiver for a dependent whose link carries canRequestExport. Otherwise PermissionError.

Notes need three things to be included: the organisation has the notes capability, notes appears explicitly in include, and the requester is a clinician or administrator. A caregiver-initiated export never contains notes.

Naming families explicitly is better practice than '*' — an export is a disclosure, and disclosures should be deliberate. Bundles are available seven days after completion, then deleted.


notes

playspace.notes.list(options?): Paginated<Note>
playspace.notes.get(noteId): Promise<Note>
playspace.notes.versions(noteId): Promise<NoteVersion[]>
playspace.notes.templates(): Promise<NoteTemplate[]>

Read-only. There is no write method and there will not be one — your platform owns clinical documentation.

Requires the notes capability on the organisation, which is off by default. Without it every call throws PermissionError.


changes

playspace.changes.list(options?): Promise<{ data, nextCursor, hasMore }>
playspace.changes.subscribe(options): Promise<void>

One ordered, resumable feed of everything that changed. There are no webhooks; you poll this.

const changes = await playspace.changes.list({ since: cursor, limit: 100 })
for (const change of changes.data) { ... }
await store.setCursor(changes.nextCursor)

For a worker, subscribe manages the loop:

await playspace.changes.subscribe({
  cursor: await store.getCursor(),
  onChange: async (change) => { await handle(change) },
  onCursor: async (cursor) => { await store.setCursor(cursor) },
  interval: '30s',
  signal: abortController.signal,
})

It invokes onChange in order and advances the cursor only after your handler resolves, so a throw stops at the last successfully processed entry — nothing lost, nothing silently skipped.

The cursor is opaque and monotonic. Entries are retained thirty days; a cursor older than that throws ValidationError. A poll that finds nothing is cheap and does not count against your rate limit.

Payloads carry identifiers, statuses, counts and timestamps. Never a name, never clinical content. Fetch the object if you need it.


usage

playspace.usage.get(): Promise<Usage>
playspace.usage.byClinician(options?): Paginated<{ clinician, counts }>
playspace.usage.history({ periods? }): Promise<Usage[]>
const usage = await playspace.usage.get()
// { period: '2026-09', resources: { storybooks: { used: 214, limit: 1000 } }, resetsAt: '...' }

Warn at eighty percent in your own interface. A clinician who discovers an exhausted quota halfway through a session with a seven-year-old will contact you, not us.


Errors

Every error extends PlaySpaceError and carries type, status, requestId and detail.

Class Thrown for Extra properties
AuthenticationError 401
PermissionError 403 requiredScopes, grantedScopes, surface
NotFoundError 404
ConflictError 409
ValidationError 422 fields, origin
RateLimitError 429 rate limit retryAfter
QuotaExceededError 429 quota resource, resetsAt
ServiceError 5xx retryAfter
JobFailedError job.wait() on failure jobId, reason
TimeoutError job.wait() on timeout jobId

Branch on the class or on type, never on message — messages are prose and may be reworded.

404 covers three cases on purpose: absent, archived, and belonging to another organisation are deliberately indistinguishable, because distinguishing them is exactly the signal an enumeration attack needs.

Full catalogue with causes and fixes: error reference.


Testing

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

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

The whole surface in memory, no network. Generation resolves against fixtures, so job.wait() returns instantly and a suite is fast, deterministic and free.

Seed it with your own data:

const playspace = new PlaySpaceMock({
  seed: 'deterministic',
  fixtures: { patients: [{ externalId: 'client_55130', firstName: 'A.', lastName: 'R.' }] },
})

Assert on failure paths without provoking them:

playspace.mock.failNext('storybooks.generate', new QuotaExceededError({ resource: 'storybooks' }))