Guide: Testing, then going live
Two environments, and no sandbox
There is no PlaySpace sandbox. No permanently addressable synthetic tenant, no stable fixture caseload, no weekly reset, no captured-message mailbox, and no self-serve key. If you have read about one, it did not ship.
What exists is a development environment and a production environment, both of them the real platform.
| Development | Production | |
|---|---|---|
| Base URL | https://agentic-ps-dev.playspace.health |
https://agentic-ps.playspace.health |
| Credentials | Apply on that host, issued on approval | Apply on that host, issued on approval |
| Authentication | Real machine-to-machine client credentials | Real machine-to-machine client credentials |
| Rate limits, tenancy, row-level security | Enforced identically | Enforced identically |
| Generation | Real, and it really costs money | Real |
| Data | Whatever you create; it persists | Real clinical data |
A credential is bound to its environment. A development credential is refused by production and a production credential is refused by development, so there is no configuration in which a test run reaches real clinical data.
Both hosts serve an interactive console at /docs, alongside the API itself. Unlike the reference on this documentation site, that one issues real requests — it shares an origin with the endpoints. It is the fastest way to confirm a freshly issued credential works before any code exists.
Mint the token in a terminal and paste it into the console's Bearer Token field; its Authorize button cannot mint one for you, because a browser is not allowed to perform a client-secret token exchange. For the content surfaces, paste a delegated token. Full steps: trying a call before you write code.
You build your own fixtures. Development starts empty for you. Create your clinics, practitioners and patients through the API in a setup step, and either tear them down or reuse them — nothing resets them for you, and nothing will delete them out from under a test run either. That is a fair trade for an environment that behaves exactly like production, but it does mean the first thing you write is a seeding script rather than a first assertion.
Nothing captures outbound messages. There is no endpoint that lets you read what the platform sent, so use email addresses you actually control for anything you create.
Getting credentials
Apply through the registration form on the environment you want: /partner-register on the host above. You describe the integration and pick the access it needs, verify your email with a press on the emailed page, and the application goes to review. The decision arrives by email, with a reason if it is a rejection. The whole path is get your credentials.
On approval, the credential is provisioned automatically and delivered by a one-time link that expires 72 hours later. The page shows the client identifier, the client secret, the audience and the base URL, once — store the secret in your secret manager before you close it. If the link expires first, reply to the approval email: PlaySpace can reveal the same secret again inside that window, and rotate it for a fresh one afterwards.
Scopes are fixed on the credential at approval. To change them, ask: PlaySpace edits the grant on your credential, in either direction, and the change takes effect on your next token rather than on the token you are holding.
Delegation is a per-organisation setting and is on by default for an organisation that registered through the form. Everything content-shaped in these documents — storybooks, worksheets, forms, playrooms, toolkits, games, embed tokens — requires a delegated token issued acting as one practitioner. If PlaySpace has turned delegation off for yours, those calls answer 403 with problem type delegation-not-enabled, so check it before you build against them rather than after.
Testing your own code
There is no published test double. @playspace-health/embed ships no mock and no fixture server, so a unit test that "uses PlaySpace" is really a unit test of your own adapter with the network stubbed. Write it that way deliberately:
- Stub the HTTP layer under
createEmbedClientand assert on what your code sends — that the token it handsgetAccessTokenis the delegated one, thatoriginsis your registered origin and not a computed one, thatcapabilitiesis the narrowest set the screen needs. - Test your token route's authorisation with no PlaySpace involved at all. This is the whole security boundary of an embedded integration: PlaySpace verifies that a delegated token names a practitioner in your organisation, and cannot verify that this browser should be that practitioner. That check is yours, and it deserves the most tests in your suite.
- Test the failure branches.
PlaySpaceApiErrorcarries the API's problem document verbatim, so construct one and assert your code branches onstatusandproblem.typerather than on a message string.
Integration tests against development
Run the paths a stub cannot prove: real authentication, real delegation, real rate limits, real generation.
Good candidates, each of which has broken a real integration:
- An organisation-wide token is refused. Mint an embed token, or list storybooks, with a token carrying no delegated claim and assert
403. This catches the case where only some of your code paths delegate. - A malformed origin is rejected at mint.
https://app-*.yourclinic.comis not a valid frame-ancestors source, and the API answers422naming it rather than handing back a token that would produce a blank rectangle in a browser. - A repeated
Idempotency-Keyreplays. Start a storybook twice with the same key and assert you got the same book back, not two. This is the test that stops a retried batch generating a second invoice. - Another organisation's id answers
404, not403. Absent, deleted and belonging to somebody else are deliberately indistinguishable. - Rate-limit headers move. Read them on a normal response so your client knows where they are before it needs them.
Keep generation tests few and deliberate. Every one of them spends money, they take a minute or two to reach ready, and there is no quota to stop a loop that goes wrong.
Browser tests
The surfaces render in a cross-origin iframe, which shapes what a test can see.
You cannot reach inside the frame, and that is the point rather than an obstacle. Do not try to drive the storybook editor or select a figure in the sandtray from your test. Assert on what crosses the boundary: the frame mounted, the event you expected arrived, your own interface reacted.
test('the storybook create surface mounts', async ({ page }) => {
await page.goto('/clinician/storybooks')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.locator('iframe[data-testid="storybook-embed-frame"]')).toBeVisible()
await expect(page.getByTestId('playspace-ready')).toHaveAttribute('data-mode', 'create')
})
Surface a test hook from your own event handler so there is something stable to assert against:
<StorybookEmbed
baseUrl={PLAYSPACE_ORIGIN}
fetchToken={mintTokenOnMyServer}
mode="create"
onEvent={(event) => {
if (event.type === 'ready') setReadyMode(event.payload.mode)
}}
iframeAttributes={{ 'data-testid': 'storybook-embed-frame' }}
/>
Test the token refresh. Mint with a deliberately short ttlSeconds — the minimum is sixty seconds — and assert the frame is still alive and still the same document after the refresh window has passed. A remount on refresh is the defect this catches, and in production it looks like a clinician losing a half-written book rather than like an error.
For a two-seat game session, test both seats. Two browser contexts, two tokens from one createGameSession response, one scene. Handing a frame the other seat's token is refused rather than silently re-scoped, and a test is a cheaper place to find that out.
Going live
Credentials
- Production credentials issued and stored in a secret manager, not in an environment file that travels with a repository.
- No PlaySpace secret appears in any browser bundle. Search your built assets and confirm zero hits.
- You know who to contact for a rotation, and you have tried reading the secret back once so you already know you cannot.
- Delegation still enabled on the production credential, not only on the development one.
Origins
- Every production origin registered at mint, exactly — scheme and host, no trailing slash, no path.
- Preview and staging origins handled deliberately: either registered, or the embed is disabled outside production. A preview deployment on a domain nobody registered renders an empty rectangle.
- Your own Content Security Policy allows framing the PlaySpace origin you pass as
baseUrl. - If your page or a proxy in front of it sends a
Permissions-Policyheader, it names the PlaySpace origin forcamera,microphone,display-capture,fullscreen,autoplayandpicture-in-picture— otherwise a session surface fails before any permission prompt appears, and the header wins over theallowattribute the SDK sets.
The token route
- Authorises the caller against your own session before minting. This is the entire security boundary.
- Derives the acting practitioner from your own record of who is signed in, never from a client-supplied parameter.
- Returns only the token value and its expiry, never the whole mint response.
- Rate-limited on your side. From an attacker's point of view it is an unauthenticated-adjacent surface.
- Calls
logout()on the embed handle when your user signs out. Unmounting an iframe does not revoke a token.
Generation
-
Idempotency-Keyon every storybook generation call, keyed on something stable in your own system. - Your own counter on how many books you start, and an alert on it. There is no quota endpoint and no per-organisation ceiling, so this ceiling is yours or it does not exist.
- A clinician-readable message for the
403a practice without storybook generation gets, rather than a stack trace.
Failure handling
- Every failure logs the
requestIdfrom the response and no client name. - A failed token mint alerts somebody — it means your token route or your credential is down, and every embedded surface in your product is dark.
- Problem documents handled by
typeandstatus, not by matching on text. - The embed's
errorevent reaches your error tracker.
Data handling
- No client name and no free-text search term in any URL your application constructs.
- Storybook and worksheet titles kept out of logs and analytics — they are model-generated or clinician-written and can echo the child they were made for.
- Signed asset links (page images, covers, PDF downloads) never logged, never cached, re-read when needed.
Operations
- Polling intervals decided deliberately. There is no change feed, so everything you learn about a resource, you learn by asking.
- PlaySpace ids stored against your own records — storybook, worksheet, playroom, game session — because they are the only join between the two systems.
- Rate-limit headers read and respected rather than discovered through
429s.
Rolling out
Start with one clinic. Not one feature — one clinic, with everything you intend to ship. Feedback on a therapeutic surface is qualitative and arrives in conversation rather than in a metric, and you want that conversation with ten clinicians before it is with a thousand.
Ship the hosted session link first if you are unsure. Minting join links for an appointment is one call and two links in your interface. It puts the product in front of clinicians in days rather than a quarter, and what you learn shapes the embedded integration you build afterwards.
Watch three things in the first month. Token mint failures by status, which catch integration defects. Storybooks started per clinic, which is your only view of spend. And the ratio of sessions created to sessions actually joined, which catches an entry point nobody can find.
Getting help
Partner engineering: techstack@playspace.health.
Include the requestId from the failing response and the diagnosis is usually the same day. Include a client's name and we will ask you to redact it and resend.