typescript type declarations
The precise contract for @playspace/sdk.
/**
* @playspace/sdk — PlaySpace Partner Platform server SDK
*
* Generated from the OpenAPI 3.1 contract at
* https://api.playspace.health/v1/openapi.json, then hand-finished for
* ergonomics. A field present here is present on the wire.
*
* Server-side only. The credentials this client takes authorise everything your
* organisation can do; to render a surface in a browser, mint a session token
* with `tokens.issue()` and hand that to `@playspace/react` instead.
*
* @packageDocumentation
*/
// ─── Client ──────────────────────────────────────────────────────────────────
export interface PlaySpaceOptions {
/** Defaults to `process.env.PLAYSPACE_CLIENT_ID`. */
clientId?: string
/** Defaults to `process.env.PLAYSPACE_CLIENT_SECRET`. */
clientSecret?: string
/** Defaults to `process.env.PLAYSPACE_ENV`, then `'production'`. */
environment?: 'production' | 'sandbox'
/** Override the base URL. Rarely needed outside our own testing. */
baseUrl?: string
/** Per-request timeout in milliseconds. Default 30000. */
timeout?: number
/** Retries for transient failures. Default 3. Mutations retry only when an idempotency key makes it safe. */
maxRetries?: number
/** Extra headers on every request. */
headers?: Record<string, string>
/** Supply a custom fetch. Defaults to the global. */
fetch?: typeof fetch
}
export declare class PlaySpace {
constructor(options?: PlaySpaceOptions)
readonly organisation: OrganisationResource
readonly clinics: ClinicsResource
readonly practitioners: PractitionersResource
readonly patients: PatientsResource
readonly caregivers: CaregiversResource
readonly sessions: SessionsResource
readonly tokens: TokensResource
readonly playrooms: PlayroomsResource
readonly toolkits: ToolkitsResource
readonly games: GamesResource
readonly storybooks: StorybooksResource
readonly worksheets: WorksheetsResource
readonly forms: FormsResource
readonly studio: StudioResource
readonly models: ModelsResource
readonly jobs: JobsResource
readonly artifacts: ArtifactsResource
readonly exports: ExportsResource
readonly notes: NotesResource
readonly changes: ChangesResource
readonly usage: UsageResource
/** Escape hatch for an endpoint this SDK version does not model yet. */
request<T = unknown>(
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
path: string,
init?: { body?: unknown; query?: Record<string, unknown>; idempotencyKey?: string }
): Promise<T>
}
export default PlaySpace
// ─── References ──────────────────────────────────────────────────────────────
/**
* A reference to a resource by either identifier. Supply exactly one.
*
* Your `externalId` works everywhere a PlaySpace `id` does, so you never have to
* store a PlaySpace identifier.
*/
export type Ref = { id: string } | { externalId: string }
/** A resolved pointer, returned on every object that references another. */
export interface Reference {
id: string
externalId: string | null
}
/** A resource identifier, or a `Ref`, wherever the SDK accepts either. */
export type IdOrRef = string | Ref
// ─── Pagination ──────────────────────────────────────────────────────────────
/**
* A list result. Iterate it to page transparently, or call `.page()` to take one
* page at a time.
*
* ```ts
* for await (const patient of playspace.patients.list()) { ... }
* const first = await playspace.patients.list().page()
* ```
*/
export interface Paginated<T> extends AsyncIterable<T> {
page(): Promise<Page<T>>
/** Collect every page into one array. Guard the size before calling this on a large list. */
all(): Promise<T[]>
}
export interface Page<T> {
data: T[]
cursor: string | null
nextCursor: string | null
limit: number
hasMore: boolean
requestId: string
}
export interface ListOptions {
cursor?: string
/** 1 to 100. Default 25. */
limit?: number
}
export interface WriteOptions {
/** Overrides the key the SDK derives from the call's arguments. */
idempotencyKey?: string
}
// ─── Enumerations ────────────────────────────────────────────────────────────
export type Surface =
| 'sandtray'
| 'dollhouse'
| 'whiteboard'
| 'games'
| 'playroom'
| 'storybooks'
| 'worksheets'
| 'forms'
| 'studio'
| 'models'
export type Role = 'clinician' | 'patient'
export type PractitionerRole = 'member' | 'administrator' | 'owner'
export type CaregiverRelationship =
| 'parent'
| 'guardian'
| 'fosterCarer'
| 'grandparent'
| 'sibling'
| 'other'
export type PlayroomType = 'child' | 'teen' | 'adult'
export type SessionItem =
| 'activityShelf'
| 'multiplayerGames'
| 'singlePlayerGames'
| 'wallPosters'
| 'whiteboard'
| 'sandTray'
| 'dollhouse'
export type GameProvider = 'playspace' | 'foony' | 'papergames' | 'studio'
export type ContentStatus = 'generating' | 'ready' | 'failed' | 'archived'
export type PublishTarget = 'false' | 'playroomToolkit' | 'clinicLibrary' | 'community'
export type SessionStatus = 'pending' | 'live' | 'complete' | 'cancelled'
export type JobStatus = 'queued' | 'running' | 'complete' | 'failed' | 'cancelled'
export type JobType =
| 'storybook'
| 'storybookPage'
| 'worksheet'
| 'form'
| 'formExtraction'
| 'game'
| 'model'
| 'export'
export type ArtifactType =
| 'storybook'
| 'storybookCopy'
| 'worksheet'
| 'worksheetCopy'
| 'form'
| 'formResponse'
| 'sandtraySave'
| 'dollhouseSave'
| 'whiteboardSnapshot'
| 'generatedGame'
| 'model'
| 'sessionSummary'
| 'note'
export type DownloadFormat = 'pdf' | 'epub' | 'png' | 'svg' | 'glb' | 'zip' | 'csv' | 'json'
/**
* Durations in request parameters. Minutes or hours.
* @example '15m' | '60m' | '24h'
*/
export type Duration = `${number}m` | `${number}h`
// ─── Organisation ────────────────────────────────────────────────────────────
export interface Organisation {
id: string
name: string
status: 'active' | 'suspended' | 'revoked'
/** Product areas enabled for this organisation, such as `notes`. */
capabilities: string[]
createdAt: string
}
export interface SurfaceAvailability {
surface: Surface
licensed: boolean
/** Whether this surface can render in a partner-hosted frame. */
embeddable: boolean
}
export interface PracticeGroup {
id: string
externalId: string | null
name: string
parent: Reference | null
clinicCount: number
}
export interface OrganisationResource {
get(): Promise<Organisation>
/** Render your own entry points from this. A surface absent here is refused at mint. */
surfaces(): Promise<SurfaceAvailability[]>
groups(options?: ListOptions): Paginated<PracticeGroup>
}
// ─── Identity ────────────────────────────────────────────────────────────────
export interface Clinic {
id: string
externalId: string | null
name: string
country: string
timezone: string | null
group: Reference | null
createdAt: string
archivedAt: string | null
}
export interface ClinicWrite {
externalId: string
name: string
/** Two-letter country code. */
country: string
timezone?: string
group?: Ref
}
export interface Practitioner {
id: string
externalId: string | null
clinic: Reference
firstName: string
lastName: string
email: string | null
role: PractitionerRole
country: string | null
createdAt: string
archivedAt: string | null
}
export interface PractitionerWrite {
externalId: string
clinic: Ref
firstName: string
lastName: string
email?: string
role?: PractitionerRole
/**
* Required for regional availability of some surfaces. Omit it and those
* surfaces fail closed for this practitioner.
*/
country?: string
}
export interface Patient {
id: string
externalId: string | null
clinic: Reference
firstName: string
lastName: string
dateOfBirth: string | null
caregivers: CaregiverLink[]
createdAt: string
archivedAt: string | null
}
export interface PatientWrite {
externalId: string
clinic: Ref
/** An initial is acceptable. Send the minimum a clinician needs to recognise the right person. */
firstName: string
lastName: string
dateOfBirth?: string
/** Clinicians whose caseload this client is on. */
practitioners?: Ref[]
caregivers?: Array<{ externalId: string; relationship: CaregiverRelationship }>
}
export interface CaregiverLink {
id: string
patient: Reference
caregiver: Reference
relationship: CaregiverRelationship
receivesSessionLinks: boolean
canRequestExport: boolean
}
export interface ClinicsResource {
list(options?: ListOptions & { group?: string; includeArchived?: boolean }): Paginated<Clinic>
/** Creates on first sight, updates thereafter. Keyed on `externalId`. */
upsert(body: ClinicWrite, options?: WriteOptions): Promise<Clinic>
get(id: IdOrRef): Promise<Clinic>
update(id: IdOrRef, body: Partial<ClinicWrite>, options?: WriteOptions): Promise<Clinic>
/** Soft. A clinic with active practitioners or patients is refused. */
archive(id: IdOrRef, options?: WriteOptions): Promise<void>
}
export interface PractitionersResource {
list(
options?: ListOptions & { clinic?: string; includeArchived?: boolean }
): Paginated<Practitioner>
upsert(body: PractitionerWrite, options?: WriteOptions): Promise<Practitioner>
get(id: IdOrRef): Promise<Practitioner>
update(
id: IdOrRef,
body: Partial<PractitionerWrite>,
options?: WriteOptions
): Promise<Practitioner>
archive(id: IdOrRef, options?: WriteOptions): Promise<void>
}
export interface PatientsResource {
list(
options?: ListOptions & { clinic?: string; practitioner?: string; includeArchived?: boolean }
): Paginated<Patient>
upsert(body: PatientWrite, options?: WriteOptions): Promise<Patient>
get(id: IdOrRef): Promise<Patient>
update(id: IdOrRef, body: Partial<PatientWrite>, options?: WriteOptions): Promise<Patient>
archive(id: IdOrRef, options?: WriteOptions): Promise<void>
/**
* A POST, and there is no GET equivalent. A 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.
*/
search(body: {
query: string
clinic?: string
practitioner?: string
limit?: number
}): Promise<Patient[]>
caregivers(id: IdOrRef): Promise<CaregiverLink[]>
}
export interface CaregiversResource {
link(
body: {
patient: Ref
caregiver: Ref
relationship: CaregiverRelationship
receivesSessionLinks?: boolean
canRequestExport?: boolean
},
options?: WriteOptions
): Promise<CaregiverLink>
unlink(linkId: string, options?: WriteOptions): Promise<void>
}
// ─── Sessions and tokens ─────────────────────────────────────────────────────
export interface SessionParticipant {
patient: Reference
joinedAt: string | null
}
export interface SessionLinks {
clinician: string
patient: string
expiresAt: string
}
export interface Session {
id: string
externalId: string | null
status: SessionStatus
clinician: Reference
participants: SessionParticipant[]
playroom: string | null
surfaces: Surface[]
scheduledAt: string | null
startedAt: string | null
endedAt: string | null
/** Present on create and on an explicit re-mint. Never stored. */
links: SessionLinks | null
createdAt: string
}
export interface NotifyFlags {
patient?: boolean
clinician?: boolean
}
export interface SessionCreate {
appointment: { externalId: string; scheduledAt?: string }
clinician: Ref
participants: Array<{ patient: Ref }>
/** A playroom identifier or slug. Omit for the clinic's default. */
playroom?: string
/** The ceiling for this session. Omit for every licensed surface. */
surfaces?: Surface[]
notify?: NotifyFlags
}
export interface SessionSummary {
session: Reference
durationMs: number
participants: Array<{ role: Role; joinedAt: string; leftAt: string | null }>
surfacesUsed: Array<{ surface: Surface; durationMs: number }>
/**
* Three caveats before building a report. Only the clinician's play bracket is
* persisted. Every duration is an upper bound — a backgrounded tab keeps
* accruing. A remount can split one continuous play into two entries, so sum
* by game rather than assuming one entry per play.
*/
gamesPlayed: Array<{ title: string; durationMs: number; plays: number }>
artifacts: Artifact[]
}
export interface SessionsResource {
list(
options?: ListOptions & {
status?: SessionStatus
clinician?: string
patient?: string
scheduledAfter?: string
scheduledBefore?: string
}
): Paginated<Session>
create(body: SessionCreate, options?: WriteOptions): Promise<Session>
get(id: IdOrRef): Promise<Session>
update(
id: IdOrRef,
body: { scheduledAt?: string; playroom?: string; surfaces?: Surface[]; notify?: NotifyFlags },
options?: WriteOptions
): Promise<Session>
end(id: IdOrRef, options?: WriteOptions): Promise<Session>
cancel(
id: IdOrRef,
body?: { reason?: string; notify?: NotifyFlags },
options?: WriteOptions
): Promise<Session>
summary(id: IdOrRef): Promise<SessionSummary>
/** Hosted links are ephemeral. Mint them when you are about to show them. */
links(id: IdOrRef, options?: WriteOptions): Promise<SessionLinks>
}
export interface SessionToken {
id: string
value: string
role: Role
surfaces: Surface[]
expiresAt: string
}
export interface TokenCreate {
session: string
subject: Ref
role: Role
/** Never wider than the session's ceiling. Omit to inherit it. */
surfaces?: Surface[]
/**
* Host origins allowed to frame this surface. Validated at mint, so a typo is a
* readable error rather than a blank frame in production.
*/
origins: string[]
/** Maximum `'60m'`. Default `'15m'`. Short by design; the React SDK renews silently. */
ttl?: Duration
}
export interface TokensResource {
issue(body: TokenCreate, options?: WriteOptions): Promise<SessionToken>
revoke(tokenId: string, options?: WriteOptions): Promise<void>
}
// ─── Content ─────────────────────────────────────────────────────────────────
export interface ContentRef {
type: 'storybook' | 'worksheet' | 'form' | 'game' | 'generatedGame'
id?: string
/** For catalog games. */
slug?: string
order?: number
}
export interface PlayroomContent {
id: string
type: ContentRef['type']
ref: Reference
title: string
order: number
}
export interface Playroom {
id: string
title: string
type: PlayroomType
theme: string | null
items: SessionItem[]
colourPalette: string | null
contents: PlayroomContent[]
createdAt: string
}
export interface PlayroomWrite {
title: string
type: PlayroomType
theme?: string
items?: SessionItem[]
colourPalette?: string
contents?: ContentRef[]
}
export interface Toolkit {
id: string
title: string
description: string | null
contents: PlayroomContent[]
createdAt: string
}
export interface ToolkitWrite {
title: string
description?: string
contents?: ContentRef[]
}
export interface Game {
slug: string
title: string
description: string | null
category: string | null
provider: GameProvider
minPlayers: number
maxPlayers: number
/** Therapeutic skills the game develops. */
skills: string[]
ageRange: [number, number]
thumbnailUrl: string | null
embeddable: boolean
}
export interface PlayroomsResource {
list(options?: ListOptions & { type?: PlayroomType }): Paginated<Playroom>
create(body: PlayroomWrite, options?: WriteOptions): Promise<Playroom>
get(id: string): Promise<Playroom>
update(id: string, body: Partial<PlayroomWrite>, options?: WriteOptions): Promise<Playroom>
archive(id: string, options?: WriteOptions): Promise<void>
attach(id: string, content: ContentRef, options?: WriteOptions): Promise<PlayroomContent>
detach(id: string, contentId: string, options?: WriteOptions): Promise<void>
}
export interface ToolkitsResource {
list(options?: ListOptions): Paginated<Toolkit>
create(body: ToolkitWrite, options?: WriteOptions): Promise<Toolkit>
get(id: string): Promise<Toolkit>
update(id: string, body: Partial<ToolkitWrite>, options?: WriteOptions): Promise<Toolkit>
archive(id: string, options?: WriteOptions): Promise<void>
}
export interface GamesResource {
list(
options?: ListOptions & {
skills?: string[]
players?: number
ageMin?: number
ageMax?: number
category?: string
provider?: GameProvider
}
): Paginated<Game>
get(slug: string): Promise<Game>
}
// ─── Attribution ─────────────────────────────────────────────────────────────
/**
* The attribution triple, required on every generation call and carried on every
* artifact for the rest of its life.
*
* `clinician` is required because generation costs money and somebody owns that.
* `subject` is what makes an artifact personal — omit it and you have made
* library content. `session` ties it to a moment.
*/
export interface Attribution {
clinician: Ref
subject?: Ref
session?: Ref
}
// ─── Storybooks ──────────────────────────────────────────────────────────────
export interface StorybookCharacter {
name: string
role: 'protagonist' | 'supporting' | 'background'
/** The visual description carried into every page's image generation. */
description: string
}
export interface StorybookPage {
id: string
number: number
text: string
imageUrl: string | null
charactersInScene: string[]
regenerating: boolean
}
export interface Storybook {
id: string
title: string | null
status: ContentStatus
pageCount: number
ageGroup: string | null
readingLevel: string | null
style: string | null
published: PublishTarget
createdBy: Reference
subject: Reference | null
session: Reference | null
characters: StorybookCharacter[]
pages: StorybookPage[]
createdAt: string
}
export interface StorybookGenerate extends Attribution {
prompt: string
/** 4 to 24. Default 8. */
pages?: number
ageGroup?: '3-5' | '5-8' | '9-12' | '13-17' | (string & {})
readingLevel?: 'pre-reader' | 'grade-1' | 'grade-2' | 'grade-4' | (string & {})
style?: 'watercolour' | 'cartoon' | 'storybook-classic' | 'collage' | (string & {})
/**
* Naming characters up front locks their appearance across every page. Omit and
* they are inferred from the prompt, then locked after the first page.
*/
characters?: StorybookCharacter[]
}
export interface StorybooksResource {
list(
options?: ListOptions & { subject?: string; createdBy?: string; status?: ContentStatus }
): Paginated<Storybook>
get(id: string): Promise<Storybook>
update(
id: string,
body: { title?: string; published?: PublishTarget },
options?: WriteOptions
): Promise<Storybook>
archive(id: string, options?: WriteOptions): Promise<void>
/** Asynchronous. Two to four minutes for eight pages. */
generate(body: StorybookGenerate, options?: WriteOptions): Promise<Job<Storybook>>
pages: {
list(storybookId: string): Promise<StorybookPage[]>
updateText(pageId: string, text: string, options?: WriteOptions): Promise<StorybookPage>
/** Leaves every other page untouched and preserves character consistency. */
regenerate(
pageId: string,
body?: { prompt?: string },
options?: WriteOptions
): Promise<Job<StorybookPage>>
}
/** A personal copy that tracks reading position. */
assign(
id: string,
body: { subject: Ref; notifyCaregiver?: boolean },
options?: WriteOptions
): Promise<Artifact>
download(id: string, options: { format: DownloadFormat }): Promise<DownloadLink>
}
// ─── Worksheets ──────────────────────────────────────────────────────────────
export interface Worksheet {
id: string
title: string
status: ContentStatus
pageCount: number
ageGroup: string | null
createdBy: Reference
subject: Reference | null
published: PublishTarget
createdAt: string
}
export interface WorksheetWrite {
title: string
ageGroup?: string
published?: PublishTarget
}
export interface WorksheetGenerate extends Attribution {
prompt: string
/** 1 to 12. Default 3. */
pages?: number
ageGroup?: string
includeImagery?: boolean
}
export interface WorksheetCopy {
id: string
worksheet: Reference
subject: Reference
createdBy: Reference
session: Reference | null
status: 'assigned' | 'inProgress' | 'complete'
dueAt: string | null
completedAt: string | null
}
export interface WorksheetsResource {
list(options?: ListOptions & { subject?: string; createdBy?: string }): Paginated<Worksheet>
create(body: WorksheetWrite, options?: WriteOptions): Promise<Worksheet>
get(id: string): Promise<Worksheet>
update(id: string, body: Partial<WorksheetWrite>, options?: WriteOptions): Promise<Worksheet>
archive(id: string, options?: WriteOptions): Promise<void>
generate(body: WorksheetGenerate, options?: WriteOptions): Promise<Job<Worksheet>>
assign(
id: string,
body: { subject: Ref; dueAt?: string; notifyCaregiver?: boolean },
options?: WriteOptions
): Promise<Artifact>
/** A completed copy is its own artifact, distinct from the blank it came from. */
copies: {
list(
options?: ListOptions & {
subject?: string
worksheet?: string
status?: WorksheetCopy['status']
}
): Paginated<WorksheetCopy>
}
download(id: string, options: { format: DownloadFormat }): Promise<DownloadLink>
}
// ─── Forms ───────────────────────────────────────────────────────────────────
export type FormFieldType =
| 'shortText'
| 'longText'
| 'number'
| 'singleSelect'
| 'multiSelect'
| 'scale'
| 'date'
| 'signature'
| 'file'
| 'imageChoice'
export interface FormField {
id: string
label: string
type: FormFieldType
required: boolean
options?: string[]
scale?: { min: number; max: number; labels?: string[] }
/** Show this field only when another field holds a given value. */
conditional?: { fieldId: string; equals: unknown } | null
}
export interface Form {
id: string
title: string
status: ContentStatus
scored: boolean
fields: FormField[]
createdBy: Reference
published: PublishTarget
createdAt: string
}
export interface FormWrite {
title: string
scored?: boolean
fields: FormField[]
published?: PublishTarget
}
export interface FormResponse {
id: string
form: Reference
subject: Reference
submittedBy: { role: 'patient' | 'caregiver' | 'clinician'; ref: Reference }
session: Reference | null
answers: Array<{ fieldId: string; label: string; type: FormFieldType; value: unknown }>
/** Present for scored instruments only. */
score: number | null
submittedAt: string
}
export interface FormsResource {
list(options?: ListOptions & { createdBy?: string }): Paginated<Form>
create(body: FormWrite, options?: WriteOptions): Promise<Form>
get(id: string): Promise<Form>
update(id: string, body: Partial<FormWrite>, options?: WriteOptions): Promise<Form>
archive(id: string, options?: WriteOptions): Promise<void>
generate(
body: Attribution & { prompt: string; maxFields?: number; scored?: boolean },
options?: WriteOptions
): Promise<Job<Form>>
/**
* Build a structured, fillable form from an existing paper intake sheet. The
* most common request from clinics with twenty years of paper.
*/
extract(
body: { clinician: Ref; file: Uint8Array | Blob; filename: string },
options?: WriteOptions
): Promise<Job<Form>>
/** Set `routeToCaregiver` for a paediatric client so the link reaches the adult. */
assign(
id: string,
body: { subject: Ref; routeToCaregiver?: boolean; dueAt?: string },
options?: WriteOptions
): Promise<Artifact>
responses: {
list(
options?: ListOptions & { subject?: string; form?: string; submittedAfter?: string }
): Paginated<FormResponse>
get(responseId: string): Promise<FormResponse>
}
}
// ─── Studio and models ───────────────────────────────────────────────────────
export interface GeneratedGame {
id: string
title: string
status: ContentStatus
players: number
playUrl: string | null
createdBy: Reference
subject: Reference | null
published: PublishTarget
createdAt: string
}
export interface StudioResource {
/** Four to eight minutes. Produces a real playable application. */
generate(
body: Attribution & {
prompt: string
players?: number
duration?: 'short' | 'medium' | 'long'
},
options?: WriteOptions
): Promise<Job<GeneratedGame>>
games: {
list(
options?: ListOptions & { subject?: string; createdBy?: string }
): Paginated<GeneratedGame>
}
}
export interface Model {
id: string
title: string | null
status: ContentStatus
format: 'glb'
previewUrl: string | null
source: 'text' | 'image'
createdBy: Reference
createdAt: string
}
export type ModelGenerate =
| { clinician: Ref; session?: Ref; source: 'text'; prompt: string }
| { clinician: Ref; session?: Ref; source: 'image'; file: Uint8Array | Blob; filename: string }
export interface ModelsResource {
/**
* Three to six minutes. An uploaded photograph is used to produce the model and
* then discarded — not retained, not used for training, never in an export. The
* capability is for objects, pets and places; do not upload an image containing
* a person.
*/
generate(body: ModelGenerate, options?: WriteOptions): Promise<Job<Model>>
list(options?: ListOptions & { createdBy?: string }): Paginated<Model>
}
// ─── Jobs ────────────────────────────────────────────────────────────────────
/**
* Asynchronous work. Every generation call returns one; nothing blocks.
*
* ```ts
* const job = await playspace.storybooks.generate({ ... })
* const book = await job.wait({ timeout: '5m' })
* ```
*/
export interface Job<T = unknown> {
id: string
type: JobType
status: JobStatus
/** 0 to 100. */
progress: number
estimatedSeconds: number | null
/** Present when `status` is `'complete'`. */
result: T | null
/**
* Present when `status` is `'failed'`. Written for a clinician rather than an
* engineer. A failed job does not consume quota.
*/
reason: string | null
createdBy: Reference
createdAt: string
completedAt: string | null
/** Poll until terminal. Rejects with `JobFailedError` on failure. */
wait(options?: { timeout?: Duration; pollInterval?: Duration }): Promise<T>
/** Re-read the job's current state. */
refresh(): Promise<Job<T>>
cancel(): Promise<Job<T>>
}
export interface JobsResource {
list(options?: ListOptions & { status?: JobStatus; type?: JobType }): Paginated<Job>
get(jobId: string): Promise<Job>
cancel(jobId: string, options?: WriteOptions): Promise<Job>
}
// ─── Artifacts ───────────────────────────────────────────────────────────────
/**
* Everything PlaySpace produced and kept.
*
* The attribution triple is why the client library, the export bundle, the spend
* report and the session summary are each one query.
*/
export interface Artifact {
id: string
type: ArtifactType
title: string | null
status: ContentStatus
/** The clinician. Always present. */
createdBy: Reference
/** The client it was made for. Null for library content. */
subject: Reference | null
/** Where it happened. Null for content created outside a session. */
session: Reference | null
formats: DownloadFormat[]
createdAt: string
}
export interface DownloadLink {
url: string
format: DownloadFormat
sizeBytes: number
expiresAt: string
}
export interface ArtifactsResource {
list(
options?: ListOptions & {
subject?: string
createdBy?: string
session?: string
type?: ArtifactType
createdAfter?: string
}
): Paginated<Artifact>
get(artifactId: string): Promise<Artifact>
/**
* A short-lived, single-use link. Request it when you are ready to stream it.
*
* `'json'` is available for every type and is always complete: no rendered
* format carries information its structured form omits.
*/
download(artifactId: string, options: { format: DownloadFormat }): Promise<DownloadLink>
}
// ─── Exports ─────────────────────────────────────────────────────────────────
export type ExportInclude =
| '*'
| 'storybooks'
| 'worksheets'
| 'formResponses'
| 'sandtraySaves'
| 'dollhouseSaves'
| 'whiteboardSnapshots'
| 'generatedGames'
| 'models'
| 'sessionSummaries'
| 'notes'
export type ExportReason =
| 'clientRequest'
| 'caregiverRequest'
| 'clinicalTransfer'
| 'migration'
| 'legal'
| 'internalReview'
export interface ExportCreate {
/** Required unless `scope` is `'organisation'`. */
subject?: Ref
scope?: 'subject' | 'organisation'
/** Naming families explicitly is better practice — an export is a disclosure. */
include: ExportInclude[]
format?: 'bundle' | 'json'
/** Must resolve to somebody with standing to make this disclosure. */
requestedBy: Ref
reason: ExportReason
}
export interface Export {
id: string
scope: 'subject' | 'organisation'
subject: Reference | null
include: ExportInclude[]
format: 'bundle' | 'json'
status: 'queued' | 'running' | 'complete' | 'failed' | 'expired'
progress: number
sizeBytes: number | null
partCount: number
requestedBy: Reference
reason: ExportReason
createdAt: string
/** Seven days after completion. Request it again if you need it again. */
expiresAt: string | null
wait(options?: { timeout?: Duration }): Promise<ExportDownload>
refresh(): Promise<Export>
}
export interface ExportDownload {
parts: Array<{ url: string; part: number; sizeBytes: number }>
expiresAt: string
}
export interface ExportsResource {
list(options?: ListOptions & { subject?: string }): Paginated<Export>
create(body: ExportCreate, options?: WriteOptions): Promise<Export>
get(exportId: string): Promise<Export>
download(exportId: string): Promise<ExportDownload>
}
// ─── Notes ───────────────────────────────────────────────────────────────────
export interface Note {
id: string
subject: Reference
author: Reference
session: Reference | null
template: Reference
status: 'draft' | 'final' | 'amended'
version: number
sections: Array<{ heading: string; body: string }>
finalisedAt: string | null
createdAt: string
}
export interface NoteVersion {
version: number
author: Reference
isActive: boolean
createdAt: string
}
export interface NoteTemplate {
id: string
name: string
sections: string[]
}
/**
* 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.
*/
export interface NotesResource {
list(
options?: ListOptions & { subject?: string; session?: string; finalisedAfter?: string }
): Paginated<Note>
get(noteId: string): Promise<Note>
versions(noteId: string): Promise<NoteVersion[]>
templates(): Promise<NoteTemplate[]>
}
// ─── Change feed ─────────────────────────────────────────────────────────────
export type ChangeType =
| 'session.created'
| 'session.started'
| 'session.complete'
| 'session.cancelled'
| 'artifact.created'
| 'artifact.ready'
| 'artifact.deleted'
| 'job.progress'
| 'job.complete'
| 'job.failed'
| 'form.submitted'
| 'worksheet.completed'
| 'note.available'
| 'export.ready'
| 'quota.threshold'
/**
* One entry in the change feed.
*
* Payloads carry identifiers, statuses, counts, durations and enumerated values.
* Never a name, never clinical content. Fetch the object if you need it.
*/
export interface Change {
/** Monotonic within your organisation. */
sequence: number
type: ChangeType
occurredAt: string
artifact: Reference | null
session: Reference | null
subject: Reference | null
job: Reference | null
data: Record<string, unknown>
}
export interface ChangesResource {
/** Omit `since` to start from now. Entries are retained thirty days. */
list(options?: {
since?: string
limit?: number
types?: ChangeType[]
}): Promise<{ data: Change[]; nextCursor: string; hasMore: boolean }>
/**
* A managed poll loop for a worker process. Invokes `onChange` in order and
* advances the cursor only after it resolves, so a throw stops at the last
* successfully processed entry — nothing lost, nothing silently skipped.
*/
subscribe(options: {
cursor?: string
onChange: (change: Change) => Promise<void> | void
onCursor?: (cursor: string) => Promise<void> | void
onError?: (error: PlaySpaceError) => Promise<void> | void
types?: ChangeType[]
interval?: Duration
signal?: AbortSignal
}): Promise<void>
}
// ─── Usage ───────────────────────────────────────────────────────────────────
export interface Usage {
/** `'2026-09'`. */
period: string
resources: Record<string, { used: number; limit: number }>
resetsAt: string
}
export interface UsageResource {
get(): Promise<Usage>
byClinician(
options?: ListOptions & { createdBy?: string }
): Paginated<{ clinician: Reference; counts: Record<string, number> }>
history(options?: { periods?: number }): Promise<Usage[]>
}
// ─── Errors ──────────────────────────────────────────────────────────────────
export declare class PlaySpaceError extends Error {
/** The stable problem type. Branch on this, never on `message`. */
readonly type: string
readonly status: number
readonly requestId: string | null
readonly detail: string | null
}
export declare class AuthenticationError extends PlaySpaceError {}
export declare class PermissionError extends PlaySpaceError {
readonly requiredScopes: string[] | null
readonly grantedScopes: string[] | null
readonly surface: Surface | null
}
export declare class NotFoundError extends PlaySpaceError {}
export declare class ConflictError extends PlaySpaceError {}
export declare class ValidationError extends PlaySpaceError {
readonly fields: Array<{ path: string; message: string }>
/** Present on `origin-not-allowed`. */
readonly origin: string | null
}
export declare class RateLimitError extends PlaySpaceError {
/** Seconds. */
readonly retryAfter: number
}
export declare class QuotaExceededError extends PlaySpaceError {
readonly resource: string
readonly resetsAt: string
}
export declare class ServiceError extends PlaySpaceError {
readonly retryAfter: number | null
}
/** Thrown by `job.wait()` when the job reaches `'failed'`. */
export declare class JobFailedError extends PlaySpaceError {
readonly jobId: string
/** Written for a clinician rather than an engineer. */
readonly reason: string
}
/** Thrown by `job.wait()` and `export.wait()` when the timeout elapses. The work continues. */
export declare class TimeoutError extends PlaySpaceError {
readonly jobId: string
}