openapi: 3.1.0
info:
  title: PlaySpace Partner API
  version: 0.1.0
  description: |-
    Partner-scoped REST API for managing clinics, practitioners, patients, and appointments. All endpoints live under the base path `/v1/partner`. Each partner organization sees only its own data.

    ## Authentication

    Authenticate with an OAuth2 client-credentials (machine-to-machine) access token issued by Auth0 for your partner organization. Send it on every request as `Authorization: Bearer <token>`. Verify your token and connectivity with `GET /v1/partner/health` before calling any business endpoint. Scopes follow a `resource:verb` naming convention — for example `clinics:read` or `patients:write` — and each operation documents the exact scope it requires.

    ## Trying these endpoints from the console

    The API hosts serve this reference with a live console at `/docs` (and `/docs/practitioner` for the practitioner tier). To make a call from it:

    1. **Mint a token in a terminal** using the client-credentials request above, and read `access_token` from the response.
    2. **Paste it into the `Bearer Token` field** in the Authentication panel. It applies to every operation on the page.
    3. Open an operation, press **Test Request**, fill in any parameters, then **Send Request**.

    **Do not use the Authorize button on the OAuth2 scheme.** It cannot work from a browser: the authorization server sends no cross-origin resource sharing headers on its token endpoint for a client-secret grant — deliberately, because a client secret must never be held by a browser — so the exchange is blocked before any response can be read. The failure surfaces as a bare browser error and looks exactly like a problem with your credential when it is nothing of the kind. The OAuth2 scheme is there to document how a token is obtained; `Bearer Token` is the field to use.

    Do not add scopes in the panel either. Your grant is fixed on the credential at approval and travels inside the token; requesting one you do not hold fails the exchange with `403 access_denied` before this API is reached at all.

    Most of this surface needs a **delegated** token rather than an organization one — see the next section. Switching between the two is just re-pasting the `Bearer Token` field.

    ## Acting as a clinician

    An **organization token** sees every clinic, practitioner, patient, and appointment your organization owns. A **delegated token** is scoped to one practitioner and only ever touches that clinician's own data. To request one, add a `practitioner_id` parameter (one of your practitioner ids) to the client-credentials token request; the authorization server confirms the practitioner belongs to your organization and binds it into the token.

    **A delegated token is required, not optional, for most of this API.** Everything a clinician personally owns — worksheets, storybooks, playrooms, toolkits, forms, game saves, game sessions, notes, exports, and embed tokens — belongs to one practitioner rather than to an organization, so an organization token has nobody to scope to and every one of those endpoints refuses it with `403`. That is 53 of the 76 operations here. Only the directory, scheduling and session-configuration surfaces (health, clinics, practitioners, patients, appointments, session config) accept an organization token. Plan for minting delegated tokens before you build against the content surfaces; availability depends on your onboarding configuration, so contact PlaySpace to enable it if a `practitioner_id` token request is rejected.

    A delegated token narrows what you see and do. Reads return only that clinician's data: the patients list and get return only their roster, and the appointments list and get return only their appointments. The practitioners and clinics directories are unchanged — both remain organization-wide on either token. Writes must be self-attributed: creating an appointment requires `partner_practitioner_id` to equal the token's clinician, and creating a patient requires `created_by_partner_practitioner_id` to equal it, otherwise the request is rejected with a `422`. Updates are limited to that clinician's own appointments and roster patients. Organization-level writes — any practitioner or clinic create, update, or delete, and deleting a patient — require an organization token and return `403` on a delegated token.

    An id outside a delegated token's scope is indistinguishable from one that never existed: both return `404`, so the response cannot be used to probe what lies outside the clinician's data.

    You stay in control of each clinician's access. Set a practitioner's `status` to `disabled` (`PATCH /v1/partner/practitioners/{id}`) and every delegated token for that clinician is rejected with `403` and the `practitioner-disabled` problem type until you set `status` back to `active`. Disabling is reversible and keeps all records — it is the right tool for suspending access, while removal (`DELETE`) is for off-boarding. Organization tokens are unaffected, so you can always see and re-enable a disabled practitioner, and PlaySpace support can also restore access if needed.

    Per-clinician tokens multiply token issuance — one per clinician rather than one per organization — so cache one delegated token per clinician and reuse it until shortly before it expires rather than minting a new token per request.

    ## Tenancy

    Every request is scoped to the partner organization the token was issued to. You never send an organization id in a request — it is derived from the token. Ids that belong to another organization behave exactly like ids that never existed: both return `404`. There is no way to observe, enumerate, or address another organization's data.

    ## Errors

    Every non-2xx response is an RFC 9457 problem document with content type `application/problem+json`. Each problem body carries `type` (an absolute URI identifying the problem class), `title`, `status`, and `detail`, plus problem-specific extension fields (for example `required_scopes` on a 403 or dependent counts on a 409). Every response — success or error — carries an `X-Request-Id` header; quote it when contacting support.

    ## Idempotency

    Every `POST`, `PATCH`, and `DELETE` requires an `Idempotency-Key` header: an opaque, caller-chosen string unique to one logical operation. The key makes retries safe:

    - A retry with the **same key and the same payload**, within 24 hours of the original call, returns the original call's cached response verbatim — the operation is not performed twice.
    - A retry with the **same key but a different payload** returns `422` with problem type `idempotency-key-mismatch`.
    - A retry sent with the **same key while the original call is still in flight** returns `409` with problem type `idempotency-conflict`; retry once the original settles.

    Keys live for 24 hours; after that the same key starts a fresh operation.

    ## Rate limits

    Limits are applied per partner organization across two windows: per minute and per hour. Every response carries `X-RateLimit-Minute-Remaining` / `X-RateLimit-Minute-Reset` and `X-RateLimit-Hour-Remaining` / `X-RateLimit-Hour-Reset`, where each `-Reset` is the time the window refills. The more restrictive window governs. Exceeding either limit returns `429`; the response includes `Retry-After`.

    ## Pagination

    List endpoints are cursor-based. Pass `limit` to set page size — it defaults to 25 and must be between 1 and 100. A larger value is rejected with a 422 rather than clamped, so a page you receive is never quietly smaller than the one you asked for. To fetch the next page, pass the `cursor` value returned in `meta.pagination.next_cursor`; treat the cursor as opaque and never construct one by hand. `next_cursor` is `null` when the list is fully consumed. Any filters you supply are applied before pagination, so they hold consistently across pages.

    ## Deletion and 404 policy

    Deletes are soft: a deleted resource is de-listed rather than destroyed. A deleted resource returns `404` on `GET` and is omitted from list responses, unless the list endpoint supports `include_deleted=true` and you set it. A nonexistent id, another organization's id, and a deleted id all return one identical `404`, and its `detail` never echoes the id you supplied — so ids cannot be probed or enumerated by their response. Repeating a `DELETE` on the same id returns that same `404`; only an `Idempotency-Key` replay of the original delete observes the prior success.
tags:
  - name: Meta
    description: Service health and connectivity. Use these endpoints to confirm a token authenticates and the API is reachable before exercising any business endpoint.
  - name: Practitioners
    description: Clinic-bound provider records. Every practitioner belongs to exactly one clinic. A practitioner’s email maps to a login identity, so an email already in use returns `409`. Deleting a practitioner de-lists rather than destroys it.
  - name: Clinics
    description: "Clinics are the partition that practitioners, patients, and appointments belong to. Deleting a clinic de-lists rather than destroys it. A clinic cannot be deleted while it still has active practitioners, patients, or appointments: that returns `409` with problem type `clinic-not-empty` and counts of the blocking dependents. Clear or reassign the dependents first."
  - name: Patients
    description: "Patient records are maintained per organization: a patient created by your organization is yours alone, and the same person may exist independently under another organization. Email is not unique — duplicate emails within or across organizations are legitimate, because families commonly share an inbox. These records carry protected health information; handle and store them accordingly. A delegated token (see \"Acting as a clinician\") narrows this surface to the token clinician's roster."
  - name: Playrooms
    description: 'Playrooms are the themed virtual spaces a clinician runs a session in: a name, an age band, a colour theme, and the set of session items available inside. Each playroom belongs to exactly one practitioner, so this surface requires a delegated token (see "Acting as a clinician") on every verb; an organization token is refused. Every new practitioner account starts with a set of PlaySpace-created rooms, flagged `is_default`. There is no delete endpoint: unpublish a room with `PATCH` setting `published` to `false`.'
  - name: Toolkits
    description: Toolkits are named bundles of session items, using the same item vocabulary as a playroom but without the room theming. Like playrooms they belong to a single practitioner and require a delegated token on every verb, ship as PlaySpace-created defaults on a new account, and have no delete endpoint — unpublish with `PATCH` setting `published` to `false`.
  - name: Appointments
    description: "Appointments link one practitioner and one patient who belong to the same clinic. There is no delete endpoint: cancel an appointment with `PATCH` setting `status` to `cancelled`. Cancelling removes the appointment from connected calendars and notifies the patient. A delegated token (see \"Acting as a clinician\") narrows this surface to the token clinician's own appointments."
  - name: Forms
    description: "Questionnaires a clinician authors and owns. Each form belongs to a single practitioner, so this surface requires a delegated token (see \"Acting as a clinician\") and always lists that clinician's own library. Only published forms are listed: a draft is still being authored, and answers recorded against one would not be meaningful. Answers are never returned here — a listed form is a blank template. To have a patient fill one, mint an embed token for the form and frame the embedded form surface."
  - name: Storybooks
    description: 'Illustrated stories a clinician creates and owns. Every endpoint here requires a delegated token (see "Acting as a clinician"): a story belongs to one clinician, so an organization-wide token has nobody to scope to and is refused. Generation is asynchronous and bills per story — the response returns the story in its `generating` state, so poll it rather than retrying the create. Image links expire about an hour after they are issued: re-read the pages for fresh links, or fetch the asset bytes, rather than storing a link.'
  - name: Games
    description: Live sandtray and dollhouse sessions, and the scenes they leave behind. Starting a session returns one ready-to-frame URL per seat — clinician and patient — both bound to the same session key, so the two participants share one scene. Every endpoint here requires a delegated token (see "Acting as a clinician"), and the clinician is read from the token rather than from the request. Deleting a saved scene de-lists it rather than destroying it. Scene preview images are served as bytes and most scenes have none, so check `has_thumbnail` before asking for one.
  - name: Notes
    description: "Clinical notes a clinician writes about a session, including the ones PlaySpace drafts automatically from a recorded session. Read-only: a note is the clinician's own record, so this API can return one but can never create, change or remove one. Every endpoint here requires a delegated token (see \"Acting as a clinician\") — a note belongs to one clinician, so an organization-wide token has nobody to scope to and is refused. Each note carries the appointment, patient and practitioner it belongs to as ids on your own records. Bodies are omitted from the list by default and returned on request; a single note always carries its body. The verbatim, speaker-diarised session recording is returned only by GET /v1/partner/notes/{id}, and only when you ask for it with `include=transcript` — never on the list, at any page size."
  - name: Exports
    description: "A complete copy of one clinician's PlaySpace records, delivered as a single ZIP archive. Every endpoint here requires a delegated token (see \"Acting as a clinician\"): an export is one clinician's whole record, so an organization-wide token has nobody to scope to and is refused. Starting one is asynchronous — the response is the export in its `queued` state, so poll it rather than retrying the start. A clinician can have only one export running at a time and that slot is shared with the exports they start themselves, so a start while one is in flight returns `409`. A finished archive stays downloadable for seven days and is then deleted. The download answers a redirect to a short-lived link: follow it and fetch the bytes promptly rather than storing the link."
  - name: Embed
    description: Short-lived tokens that let your own application frame a PlaySpace surface for one clinician. Mint them server-side, once per session, and never expose your partner credential to a browser. Requires a delegated token (see "Acting as a clinician"); the clinician the surface acts as comes from your token, never from the request body. Supply the exact origins that will frame the surface — an origin you omit cannot render it.
  - name: Clinic community
    description: "What the clinicians of one clinic have shared with each other: worksheets, storybooks and forms a practitioner published for colleagues to preview and copy into their own library. Delegated tokens act as one clinician; an organization token may list one clinic's items."
servers:
  - url: https://agentic-ps.playspace.health
    description: Production
  - url: https://agentic-ps-dev.playspace.health
    description: Development
security:
  - bearerAuth: []
  - oauth2: []
paths:
  /v1/partner/appointments:
    get:
      operationId: listPartnerAppointments
      summary: List appointments
      description: Lists your appointments. Narrow the result with `partner_clinic_id`, `partner_practitioner_id`, `partner_patient_id`, and/or `status`, and use `start_at_from` / `start_at_to` for a half-open time window (`[from, to)`) so adjacent windows tile without overlap. Results are ordered by `start_at` ascending with id as the tiebreaker, and the cursor encodes that order. Set `include_deleted=true` to also return cancelled appointments. Requires the `appointments:read` scope.
      tags:
        - Appointments
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: partner_clinic_id
          schema:
            format: uuid
            description: Optional filter. Returns only appointments belonging to this clinic. Omit to return appointments across every clinic you can access.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Optional filter. Returns only appointments belonging to this clinic. Omit to return appointments across every clinic you can access.
        - in: query
          name: partner_practitioner_id
          schema:
            format: uuid
            description: Optional filter. Returns only appointments scheduled with this practitioner. Omit to return appointments across every practitioner you can access. Combinable with the other filters.
            example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Optional filter. Returns only appointments scheduled with this practitioner. Omit to return appointments across every practitioner you can access. Combinable with the other filters.
        - in: query
          name: partner_patient_id
          schema:
            format: uuid
            description: Optional filter. Returns only appointments for this patient. Omit to return appointments across every patient you can access. Combinable with the other filters.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Optional filter. Returns only appointments for this patient. Omit to return appointments across every patient you can access. Combinable with the other filters.
        - in: query
          name: status
          schema:
            description: Optional filter. Returns only appointments in this status, one of `scheduled`, `completed`, `cancelled`, or `no_show`. Omit to return appointments in every status.
            example: scheduled
            type: string
            enum:
              - scheduled
              - completed
              - cancelled
              - no_show
          description: Optional filter. Returns only appointments in this status, one of `scheduled`, `completed`, `cancelled`, or `no_show`. Omit to return appointments in every status.
        - in: query
          name: start_at_from
          schema:
            format: date-time
            description: Optional inclusive lower bound on `start_at` (ISO-8601 offset-aware datetime). Returns only appointments starting at or after this instant. Pairs with `start_at_to` to form a half-open interval `[start_at_from, start_at_to)`, so adjacent windows tile without overlap.
            example: 2026-06-01T00:00:00.000Z
            type: string
            pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
          description: Optional inclusive lower bound on `start_at` (ISO-8601 offset-aware datetime). Returns only appointments starting at or after this instant. Pairs with `start_at_to` to form a half-open interval `[start_at_from, start_at_to)`, so adjacent windows tile without overlap.
        - in: query
          name: start_at_to
          schema:
            format: date-time
            description: Optional exclusive upper bound on `start_at` (ISO-8601 offset-aware datetime). Returns only appointments starting strictly before this instant; an appointment whose `start_at` equals this value is not returned. Pairs with `start_at_from` to form a half-open interval `[start_at_from, start_at_to)`, so adjacent windows tile without overlap or gaps.
            example: 2026-06-08T00:00:00.000Z
            type: string
            pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
          description: Optional exclusive upper bound on `start_at` (ISO-8601 offset-aware datetime). Returns only appointments starting strictly before this instant; an appointment whose `start_at` equals this value is not returned. Pairs with `start_at_from` to form a half-open interval `[start_at_from, start_at_to)`, so adjacent windows tile without overlap or gaps.
        - in: query
          name: include_deleted
          schema:
            description: When `true`, include cancelled appointments in the response. Defaults to `false`. Accepts `true`/`1`/`on` (case-insensitive) as true and `false`/`0`/`off`/empty or omission as false; any other value is rejected.
            example: false
            type: boolean
          description: When `true`, include cancelled appointments in the response. Defaults to `false`. Accepts `true`/`1`/`on` (case-insensitive) as true and `false`/`0`/`off`/empty or omission as false; any other value is rejected.
      responses:
        "200":
          description: A page of appointments, ordered by `start_at` ascending with id as the tiebreaker. `meta.pagination.next_cursor` is `null` once the list is fully consumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerAppointmentListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerAppointment
      summary: Create an appointment
      description: Books a real appointment that the clinician sees immediately. The practitioner and patient must both belong to `partner_clinic_id`; if either does not, the request is rejected with a 422 (`same-clinic-mismatch`). Conflict detection looks only at appointments your organization has already booked for this practitioner and returns a 409 on overlap; appointments booked elsewhere are not considered, and the response reveals no detail about the conflicting appointment. Booking a virtual session returns the patient and clinician join links (`patient_video_url` / `clinician_video_url`), and the appointment is added to the practitioner's connected calendar when they have one. `notify_patient` is a non-persisted flag that, when `true`, emails the patient a confirmation; the clinician confirmation and calendar updates always happen regardless. On a delegated token, `partner_practitioner_id` must equal the token's clinician, otherwise a 422. Requires the `appointments:write` scope.
      tags:
        - Appointments
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The scoping ids (`partner_clinic_id` / `partner_practitioner_id` / `partner_patient_id`), the `start_at` / `end_at` window, the required `session_type`, plus optional `timezone` / `notes` and the non-persisted `notify_patient` flag. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerAppointmentCreateBody"
      responses:
        "201":
          description: The appointment was created. The `Location` header points at the new appointment, and for a virtual session `patient_video_url` carries the patient join link and `clinician_video_url` the clinician (host) join link.
          headers:
            Location:
              description: URL of the new appointment.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerAppointmentWriteResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `appointments:write` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Conflict — either the Idempotency-Key has an in-flight original call still running (problem type `idempotency-conflict`; retriable once it settles) or the organization video policy changed during the write (problem type `video-configuration-changed`; retry with a new Idempotency-Key). Inspect `type` on the problem body to discriminate. An overlapping window for the same practitioner is NOT a conflict and is accepted.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: "Validation error. The problem `type` slug discriminates the cause: `validation-error` for a malformed body (an unknown key, an invalid scoping id, a bad `start_at` / `end_at` datetime or timezone, or `end_at` not after `start_at`) or a missing `Idempotency-Key`; `same-clinic-mismatch` when the practitioner or patient does not belong to `partner_clinic_id`; and `idempotency-key-mismatch` when the key was reused with a different body."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/appointments/{id}:
    get:
      operationId: getPartnerAppointment
      summary: Get an appointment
      description: Fetches a single appointment by its id (the id returned on list responses as `data[].id`). For a virtual appointment the response `patient_video_url` carries the patient join link. Requires the `appointments:read` scope.
      tags:
        - Appointments
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
            example: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
      responses:
        "200":
          description: The appointment at the requested id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerAppointmentResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerAppointment
      summary: Update an appointment
      description: "Partially updates an appointment: reschedule the window, change the status, and/or edit the notes. Accepts any subset of the coupled `start_at` / `end_at` pair, `timezone`, `notes`, and `status`, and at least one substantive field is required (`notify_patient` alone is a 422). A reschedule supplies `start_at` and `end_at` together (both or neither; sending one is a 422); it re-checks the partner-scoped conflict (409 on overlap) and regenerates a virtual appointment's `patient_video_url` / `clinician_video_url`. Setting `status` to `cancelled` is the full cancellation: it removes the appointment from the practitioner's calendars and, when `notify_patient` is `true`, emails the patient; it then drops from the default list (surface with `include_deleted=true`) and a later `GET`/`PATCH` returns a 404. `session_type` and the scoping ids cannot be patched. A delegated token may update only its clinician's own appointments; any other id returns a 404. Requires the `appointments:write` scope."
      tags:
        - Appointments
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
            example: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Partial-update body. At least one substantive field is required (the coupled `start_at` / `end_at` pair, `timezone`, `notes`, or `status`). Unknown keys are rejected; `session_type` and the scoping ids are not accepted.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerAppointmentUpdateBody"
      responses:
        "200":
          description: The appointment was updated. For a virtual reschedule both `patient_video_url` and `clinician_video_url` are regenerated, so they may differ from the prior values.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerAppointmentWriteResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `appointments:write` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: "Conflict. The problem `type` slug discriminates: `idempotency-conflict` when the `Idempotency-Key` has an original call still in flight (retriable once it settles); `video-configuration-changed` when the organization video policy moved during the write (retry with a new `Idempotency-Key`). A reschedule onto a window the practitioner is already booked in is NOT a conflict and is accepted."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: "Validation error. The problem `type` slug discriminates the cause: `validation-error` for a malformed body (supplying exactly one of `start_at` / `end_at`, `end_at` not after `start_at`, no substantive field, an unknown key such as `session_type` or a scoping id, a bad `status` value, datetime, or timezone), an `id` path param that is not a UUID, or a missing `Idempotency-Key`; `idempotency-key-mismatch` when the key was reused with a different body."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/appointments/{id}/session-links:
    post:
      operationId: mintPartnerAppointmentSessionLinks
      summary: Get an appointment's session join links
      description: "Returns the session entry links for an appointment — the patient link (`patient_video_url`) and the clinician (host) link (`clinician_video_url`) — for partners that store appointment ids and fetch links on demand rather than keeping the create response. Each link is a short code that redeems for a session token scoped to the appointment window, so treat them as sensitive. There is one link per person per appointment: the same two come back on every call, follow a reschedule, and stop working once the appointment is cancelled or deleted. Nothing is changed by this call. The response also reports what the links open into (`video_provider`, `waiting_room_enabled`). An in-person appointment answers with the clinician link only and `patient_video_url: null`: its session is the playroom with no video call and no waiting room. A delegated token may fetch links only for its own clinician's appointments; any other id returns a 404. Requires the `appointments:read` scope."
      tags:
        - Appointments
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
            example: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the appointment, the same id returned on list responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "200":
          description: The session entry links for the appointment — both for a virtual appointment, the clinician link alone for an in-person one.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerAppointmentSessionLinksResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `appointments:read` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: "Validation error. The problem `type` slug discriminates: `validation-error` when the `id` path param is not a UUID or the `Idempotency-Key` is missing; `idempotency-key-mismatch` when the key was reused with a different request."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/clinic-community/items:
    get:
      operationId: listClinicCommunityItems
      summary: List what the clinic has shared
      description: Lists the worksheets, storybooks and forms clinicians have shared with their clinic, newest first. On a delegated token the clinic is the acting practitioner's own and `partner_clinic_id` is refused with a 422. On an organization token `partner_clinic_id` is required, and an unknown clinic answers an empty page. Titles, descriptions and tags are clinician-authored text; render them, do not log them. Games are never listed.
      tags:
        - Clinic community
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: item_type
          schema:
            description: Return only items of one kind. Omit for every kind.
            $ref: "#/components/schemas/ClinicCommunityItemType"
          description: Return only items of one kind. Omit for every kind.
        - in: query
          name: partner_clinic_id
          schema:
            format: uuid
            description: "Organization tokens only: the clinic whose community to list, as returned by the clinics endpoints as `data[].id`. Required on an organization token and refused with a 422 on a delegated token, whose clinic is the acting practitioner's own."
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: "Organization tokens only: the clinic whose community to list, as returned by the clinics endpoints as `data[].id`. Required on an organization token and refused with a 422 on a delegated token, whose clinic is the acting practitioner's own."
      responses:
        "200":
          description: A page of shared items.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClinicCommunityListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: publishClinicCommunityItem
      summary: Share your content with the clinic
      description: Shares one worksheet, storybook or form from the acting practitioner's own library with their clinic. A copy is made for the clinic; the original stays as it was and is not affected by later changes to the shared copy. Content that is not the acting practitioner's, or that is already itself a shared copy, answers 404. Sharing the same content with the same clinic twice answers 409 `community-item-conflict`. Reuse the same Idempotency-Key on a retry to avoid a duplicate. The practice must have the community enabled. REQUIRES a delegated token.
      tags:
        - Clinic community
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Which content to share and how to present it.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClinicCommunityPublishBody"
      responses:
        "201":
          description: The item as the clinic now sees it, with its content.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClinicCommunityDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Community Item Conflict — this content is already shared with the clinic. List the clinic community to find the existing item, or remove it before sharing again.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/clinic-community/items/{id}:
    get:
      operationId: getClinicCommunityItem
      summary: Preview a shared item
      description: "Returns one shared item with its content: the pages of a worksheet or storybook with short-lived image links, or the questions of a form, in the same shape as that resource's own get-by-id. An item from another clinic, a removed item and an unknown id all answer one identical 404. REQUIRES a delegated token."
      tags:
        - Clinic community
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the clinic-community item, as returned on the list response as `data[].id`.
            example: 0d9c8b7a-6f5e-4d4c-8b3a-2f1e0d9c8b7a
          required: true
          description: ID of the clinic-community item, as returned on the list response as `data[].id`.
      responses:
        "200":
          description: The shared item and its content.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClinicCommunityDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: unpublishClinicCommunityItem
      summary: Remove an item you shared
      description: Removes a shared item from the clinic community. Only the practitioner who shared it can remove it; a colleague's item, a removed item and an unknown id all answer one identical 404, so the response never confirms an item exists. Copies colleagues already made are unaffected. REQUIRES a delegated token.
      tags:
        - Clinic community
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the clinic-community item, as returned on the list response as `data[].id`.
            example: 0d9c8b7a-6f5e-4d4c-8b3a-2f1e0d9c8b7a
          required: true
          description: ID of the clinic-community item, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The item was removed. No response body.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/clinic-community/items/{id}/copy:
    post:
      operationId: copyClinicCommunityItem
      summary: Copy a shared item into your library
      description: Makes the acting practitioner their own copy of a shared item, so they can open it, edit it and place it on their rooms. The copy is independent of the shared original. A retry with the same Idempotency-Key replays the earlier result; a deliberate second request makes a second copy. An item from another clinic, a removed item and an unknown id all answer one identical 404. The practice must have the community enabled. REQUIRES a delegated token.
      tags:
        - Clinic community
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the clinic-community item, as returned on the list response as `data[].id`.
            example: 0d9c8b7a-6f5e-4d4c-8b3a-2f1e0d9c8b7a
          required: true
          description: ID of the clinic-community item, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "201":
          description: The new copy's kind and id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ClinicCommunityCopyResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/clinics:
    get:
      operationId: listPartnerClinics
      summary: List partner clinics
      description: Returns a paginated list of your clinics. Supports cursor pagination and an `include_deleted` toggle for surfacing de-listed clinics. There is no parent-resource filter — clinics sit directly under your organization. Requires the `clinics:read` scope.
      tags:
        - Clinics
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include_deleted
          schema:
            description: When true, de-listed clinics are included in the response. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, de-listed clinics are included in the response. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
      responses:
        "200":
          description: A page of your clinics. `meta.pagination.next_cursor` is null once the list is fully consumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerClinicListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerClinic
      summary: Create a partner clinic
      description: "Creates a clinic under your organization with the supplied name and timezone. Clinic names are unique per partner and compared case-insensitively; a name already in use by another of your clinics returns 409 `clinic-name-conflict`. This endpoint has no 404 — a clinic has no parent resource that could be missing. The 201 body is the same `PartnerClinicResponse` envelope returned by `GET /v1/partner/clinics/{id}`, and the `Location` header points at the new clinic. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `clinics:write` scope."
      tags:
        - Clinics
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Clinic name and IANA timezone for the new clinic. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerClinicCreateBody"
      responses:
        "201":
          description: The clinic was created. The body matches the `GET/{id}` `PartnerClinicResponse` envelope, and the `Location` header points at the new clinic.
          headers:
            Location:
              description: Canonical URL of the new clinic — `/v1/partner/clinics/{id}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerClinicResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Two problem types share this status; inspect the `type` URI before retrying. `clinic-name-conflict` — the supplied name is already in use by another of your clinics; not retriable with the same name. `idempotency-conflict` — the `Idempotency-Key` is still in flight; retriable once it settles or with a fresh key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerClinicCreateBody` (unknown key, blank name, or an invalid IANA timezone), the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/clinics/{id}:
    get:
      operationId: getPartnerClinic
      summary: Get a partner clinic by id
      description: Returns a single clinic by id — the value surfaced on LIST responses as `data[].id`. A de-listed clinic is returned only when `include_deleted=true`; otherwise it 404s. Requires the `clinics:read` scope.
      tags:
        - Clinics
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
          required: true
          description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, the clinic is returned even if it has been de-listed. When false (the default), a de-listed clinic returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, the clinic is returned even if it has been de-listed. When false (the default), a de-listed clinic returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
      responses:
        "200":
          description: The clinic at the requested id. When `include_deleted=true` and the clinic is de-listed, `data.deleted_at` carries the timestamp at which it was de-listed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerClinicResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerClinic
      summary: Update a partner clinic
      description: "Partially updates a clinic. Accepts any subset of `name`, `timezone`; at least one is required. A PATCH whose every supplied value already equals the current value is a 200 no-op that leaves `updated_at` unchanged — setting `name` to the clinic's own current name is therefore a no-op, not a 409. Renaming to a name already in use by another of your clinics returns 409 `clinic-name-conflict` (names are unique per partner, compared case-insensitively). The 200 body is the same `PartnerClinicResponse` envelope returned by `GET /v1/partner/clinics/{id}`. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `clinics:write` scope."
      tags:
        - Clinics
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
          required: true
          description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Partial-update body. At least one of `name`, `timezone` is required. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerClinicUpdateBody"
      responses:
        "200":
          description: The clinic was updated. The body matches the `GET/{id}` `PartnerClinicResponse` envelope. A no-op PATCH (every supplied value already current) also returns 200 with an unchanged `updated_at`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerClinicResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No clinic with this id is visible to you.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Two problem types share this status; inspect the `type` URI before retrying. `clinic-name-conflict` — the supplied name is already in use by another of your clinics; not retriable with the same name. `idempotency-conflict` — the `Idempotency-Key` is still in flight; retriable once it settles or with a fresh key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerClinicUpdateBody` (unknown key, blank name, invalid IANA timezone, or no field supplied), the path `id` was not a UUID, the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerClinic
      summary: Soft-delete a partner clinic
      description: "De-lists the clinic from your roster; the underlying practice record is unaffected. The delete is rejected with 409 `clinic-not-empty` while any active practitioner, patient, or appointment still references the clinic, and is not retriable until those dependents are cleared. The 409 body carries `practitioner_count`, `patient_count`, and `appointment_count` so a UI can show what blocks the delete without another round-trip; currently only `practitioner_count` is observably non-zero — the other two are reserved and read 0. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `clinics:delete` scope."
      tags:
        - Clinics
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
          required: true
          description: UUID of the clinic, as surfaced on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The clinic was de-listed. No response body. Subsequent reads return 404 unless `include_deleted=true`.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No clinic with this id is visible to you. A repeat DELETE on an already de-listed clinic returns this same 404 (only an `Idempotency-Key` replay observes the prior success).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Two problem types share this status; inspect the `type` URI before retrying. `clinic-not-empty` — an active practitioner, patient, or appointment still references this clinic, and the body carries `practitioner_count`, `patient_count`, and `appointment_count` so a UI can show what blocks the delete; not retriable until those dependents are cleared. `idempotency-conflict` — the `Idempotency-Key` is still in flight; retriable once it settles or with a fresh key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ClinicNotEmptyProblem"
        "422":
          description: The request failed validation — the path `id` was not a UUID, the `Idempotency-Key` header was missing, or the key was reused with a different request (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/embed-tokens:
    post:
      operationId: createPartnerEmbedToken
      summary: Mint an embed capability token
      description: "Exchanges a delegated partner token for a short-lived token that authorises an embedded PlaySpace surface in an iframe. REQUIRES a delegated token: the acting practitioner is read from the pse_subject_partner_practitioner_id claim, never from the request body. Call this server-side on every page load that renders an embed; never expose the partner token to a browser."
      tags:
        - Embed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerEmbedTokenCreateBody"
      responses:
        "200":
          description: A token was minted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerEmbedTokenResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/exports:
    post:
      operationId: createPartnerExport
      summary: Start an export of the acting clinician's records
      description: "Starts building a single ZIP archive containing everything PlaySpace holds for the clinician the delegated token is acting as — their record, their caseload, and every note, form, worksheet, storybook and upload they own. There is no request body: the export is always \"all of this clinician's data\", and nothing in a request could name anyone else. Building it takes minutes and outlives this call, so the response is the export in its `queued` state rather than a finished archive; poll `GET /v1/partner/exports/{id}` with the returned id and download it once the status is `ready`. A clinician can have only one export running at a time, and that slot is shared with exports they start themselves, so this can return 409 for an export you never asked for. Started exports cannot be cancelled. REQUIRES a delegated token."
      tags:
        - Exports
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "201":
          description: The export was started. The body is the export in its `queued` state — the same shape the status endpoint returns — and the `Location` header points at it.
          headers:
            Location:
              description: Canonical URL of the new export — `/v1/partner/exports/{id}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerExportResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: "Conflict — inspect the `type` URI before retrying. `export-in-progress`: this clinician already has an export running, possibly one they started themselves in PlaySpace; there is no payload to change, so wait for it to finish and start again. `idempotency-conflict`: the `Idempotency-Key` has an in-flight original call; retriable once it settles or with a fresh key."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/exports/{id}:
    get:
      operationId: getPartnerExport
      summary: Get an export by id
      description: "Returns the current state of one export belonging to the clinician the delegated token is acting as: how far it has got, how much it has gathered, and — once it is ready — what the finished archive weighs and hashes to. This is the endpoint to poll after starting an export; a few seconds between polls is plenty, since the work takes minutes. An export belonging to another clinician, an id that never existed, and a malformed id are indistinguishable — all three return one identical 404. REQUIRES a delegated token."
      tags:
        - Exports
      parameters:
        - in: path
          name: id
          schema:
            type: string
            minLength: 1
            format: uuid
            description: ID of the export, as returned by the start operation as `data.id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the export, as returned by the start operation as `data.id`.
      responses:
        "200":
          description: The export at the requested id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerExportResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/exports/{id}/download:
    get:
      operationId: downloadPartnerExport
      summary: Download a finished export archive
      description: Hands over the finished archive as a redirect to a short-lived download link rather than as bytes on this response, because an archive can run to gigabytes and the transfer is better made directly against storage — it resumes, and nothing in between can time it out. Follow the redirect and fetch the file promptly; the link stops working after about fifteen minutes and is a credential in its own right, so do not log it, store it, or pass it on. Verify what you receive against the `archive` block on the status endpoint. An export that has not finished returns 409, one whose seven-day window has closed returns 410, and anything else returns 404. REQUIRES a delegated token.
      tags:
        - Exports
      parameters:
        - in: path
          name: id
          schema:
            type: string
            minLength: 1
            format: uuid
            description: ID of the export, as returned by the start operation as `data.id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the export, as returned by the start operation as `data.id`.
      responses:
        "302":
          description: The archive is ready. The `Location` header carries a short-lived link to the file itself; there is no response body.
          headers:
            Location:
              description: "Short-lived link to the archive. Expires after about fifteen minutes. Treat it as a credential: follow it, then discard it."
              schema:
                type: string
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: "Conflict (`export-not-ready`) — the export exists but has no archive to hand over: it is either still running or it failed. Poll the export and retry the download once its status is `ready`."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "410":
          description: Gone (`export-expired`) — the archive was kept for seven days and has been deleted. Terminal for this export and not retriable; start a new one.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/forms:
    get:
      operationId: listPartnerForms
      summary: List the acting practitioner's published forms
      description: "Returns the PUBLISHED forms owned by the practitioner the delegated token is acting as, newest first. Drafts are excluded — a draft is a work in progress and filling one would record answers against an instrument the clinician has not finished authoring. REQUIRES a delegated token; an org-wide admin token has no practitioner to scope to and is refused. Pair with an embedded form surface: mint an embed token carrying form:read (plus form:submit and a patient_id to allow saving) and frame any id from this list. Every form here is fillable through the embed; shelf_ready additionally says whether it is eligible to render on a session shelf, so the clinician could reach it from inside a live session."
      tags:
        - Forms
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
      responses:
        "200":
          description: The practitioner's published forms.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerForm
      summary: Create a form
      description: "Authors a new form owned by the practitioner the delegated token is acting as, and returns it. The same two checks the embedded builder applies are applied here — every field needs a unique id and a type the embedded surface can render — so a form created through this endpoint is one the embed can also open; a duplicate id or an unsupported type is a 422 naming the offenders. The new form is NOT shelf-ready: `published` is never set by any create path, so make it attachable with PATCH on this resource before adding it to a playroom or toolkit. `status` defaults to `draft`, which is invisible on every other endpoint here — send `published` unless you mean to finish it in PlaySpace later. REQUIRES a delegated token."
      tags:
        - Forms
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The form to create.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerFormCreateBody"
      responses:
        "201":
          description: The created form.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/forms/{id}:
    get:
      operationId: getPartnerForm
      summary: Get a form by id
      description: Returns one published form belonging to the practitioner the delegated token is acting as, in the same shape as an entry in the list. Use it to re-check `shelf_ready` after publishing rather than paging the whole library. A draft, a deleted form, another clinician's form and an unknown id all answer 404 — the endpoint does not distinguish them, so it cannot be used to probe which ids exist. REQUIRES a delegated token.
      tags:
        - Forms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the form, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the form, as returned on the list response as `data[].id`.
      responses:
        "200":
          description: The form at the requested id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerForm
      summary: Make a form shelf-ready
      description: "Makes a form eligible to be placed on a playroom or toolkit shelf, and returns it with `shelf_ready` now true. This is the step between authoring a form and attaching it: a form that is not shelf-ready is refused by the attach endpoints with `reason: not_shelf_ready`, and before this endpoint existed the only way through was for the clinician to publish it inside PlaySpace — so an integration that created a form could never put it on a shelf. Send it again on a form that is already shelf-ready and nothing changes. Only a PUBLISHED form can be made shelf-ready; a draft answers 404, because the authoring lifecycle is a separate axis this tier cannot write. REQUIRES a delegated token."
      tags:
        - Forms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the form, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the form, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The change to apply.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerFormUpdateBody"
      responses:
        "200":
          description: The form after the change, in the same shape as a get-by-id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerForm
      summary: Delete a form
      description: "Retires a form and removes it from every playroom and toolkit it sits on, so it stops appearing on any in-session activity shelf. Answers 204. SUBMISSIONS ARE KEPT: each one carries the form structure alongside the answers, so a clinical record stays readable after the template is retired — this deletes the instrument, never the responses. Deleting a form that is already gone is a 404, the same answer as an unknown id. REQUIRES a delegated token."
      tags:
        - Forms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the form, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the form, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The form was retired.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/forms/{id}/submissions:
    get:
      operationId: listPartnerFormSubmissions
      summary: List a form's submissions
      description: "Returns the answer sets recorded against one form, newest CREATED first — the order a submission was started in, not finished in, because one that has been sent and not yet answered has no submission timestamp to sort on. THIS IS THE ONE ENDPOINT ON THIS API THAT RETURNS PATIENT-AUTHORED CLINICAL CONTENT: `data` holds what a patient wrote, so render it, never log it, and keep it out of your own error reporting. Saved drafts (`status: in_progress`) are included alongside completed sets, so check `status` before treating answers as final. `patient_id` is your own patient id, mapped back from PlaySpace's; null when no patient is attached or the person was never registered through this API. Submissions OUTLIVE the form they answer: still returned after `DELETE /v1/partner/forms/{id}` retires the template, and after a clinician reverts it to a draft — both of which make `GET /v1/partner/forms/{id}` answer 404. A completed instrument is a clinical record. REQUIRES a delegated token."
      tags:
        - Forms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the form, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the form, as returned on the list response as `data[].id`.
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
      responses:
        "200":
          description: The form's submissions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormSubmissionListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/forms/{id}/submissions/{submissionId}:
    get:
      operationId: getPartnerFormSubmission
      summary: Get one submission
      description: "Returns one answer set. Same content warning as the list: `data` is patient-authored clinical content. The submission must belong to the form named in the path — a valid submission id from a different form is a 404, not a cross-form read. Like the list, it keeps answering after the form has been deleted or reverted to a draft: the record outlives the template. REQUIRES a delegated token."
      tags:
        - Forms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the form.
          required: true
          description: ID of the form.
        - in: path
          name: submissionId
          schema:
            type: string
            pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$
            format: uuid
            description: ID of the submission.
          required: true
          description: ID of the submission.
      responses:
        "200":
          description: The submission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerFormSubmissionResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/game-saves:
    get:
      operationId: listPartnerGameSaves
      summary: List the acting clinician's saved scenes
      description: Returns the sandtray and dollhouse scenes the clinician the delegated token is acting as has saved, newest first. Filter by `game_type`, by `patient_id`, or both. The scene contents are not on this response — read one scene to get them. Deleted scenes are omitted unless `include_deleted=true`. Scene names are clinician free text and can reference the child, so treat the response as clinical content. REQUIRES a delegated token — an org-wide token has no clinician to scope to and is refused.
      tags:
        - Games
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: game_type
          schema:
            description: Return only sandtray scenes, or only dollhouse ones. Omit for both.
            $ref: "#/components/schemas/PartnerGameType"
          description: Return only sandtray scenes, or only dollhouse ones. Omit for both.
        - in: query
          name: patient_id
          schema:
            format: uuid
            description: Return only the scenes filed against one of your patients. A patient id you do not own matches nothing rather than erroring.
            example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Return only the scenes filed against one of your patients. A patient id you do not own matches nothing rather than erroring.
        - in: query
          name: include_deleted
          schema:
            description: 'When true, deleted scenes are included, each carrying `is_active: false`. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.'
            example: false
            type: boolean
          description: 'When true, deleted scenes are included, each carrying `is_active: false`. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.'
      responses:
        "200":
          description: The clinician's saved scenes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerGameSaveListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/game-saves/{id}:
    get:
      operationId: getPartnerGameSave
      summary: Get one saved scene, with its contents
      description: Returns one saved scene belonging to the clinician the delegated token is acting as, including the full scene payload. That payload is what a child built in a therapy session — render or store it as clinical content, never log it. A deleted scene is a 404 unless `include_deleted=true`. REQUIRES a delegated token.
      tags:
        - Games
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the scene, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the scene, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: "When true, a deleted scene is returned (carrying `is_active: false`) instead of answering 404, and its thumbnail stays readable. Defaults to false."
            example: false
            type: boolean
          description: "When true, a deleted scene is returned (carrying `is_active: false`) instead of answering 404, and its thumbnail stays readable. Defaults to false."
      responses:
        "200":
          description: The scene at the requested id, with its contents.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerGameSaveDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerGameSave
      summary: Delete a saved scene
      description: "Removes a scene from the clinician's history. This is a soft delete: the scene drops out of the default list and its contents stop being readable, but nothing is destroyed, so it can still be surfaced with `include_deleted=true`. A repeated delete on the same id returns 404, not an error. REQUIRES a delegated token."
      tags:
        - Games
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the scene, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the scene, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The scene was deleted. No response body.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/game-saves/{id}/thumbnail:
    get:
      operationId: downloadPartnerGameSaveThumbnail
      summary: Download a scene's preview image
      description: "Returns the preview image for a scene, as image bytes rather than a link. Check `has_thumbnail` on the scene first: most scenes have no preview, and one that does not returns 404 rather than an empty response. The content type is read from the image itself. REQUIRES a delegated token."
      tags:
        - Games
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the scene, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the scene, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: "When true, a deleted scene is returned (carrying `is_active: false`) instead of answering 404, and its thumbnail stays readable. Defaults to false."
            example: false
            type: boolean
          description: "When true, a deleted scene is returned (carrying `is_active: false`) instead of answering 404, and its thumbnail stays readable. Defaults to false."
      responses:
        "200":
          description: The preview image. The content type is whatever the image actually is.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/game-sessions:
    post:
      operationId: createPartnerGameSession
      summary: Start a sandtray or dollhouse session
      description: Starts a live two-seat scene for one clinician and one patient and returns a ready-to-frame URL for each. Both seats share one session key, so whatever the clinician builds appears in the patient's frame as it happens. Call this server-side per session and hand each URL to its own participant; never expose your partner token to a browser. To refresh tokens on a session already running, call this again with that session's `game_session_id` — a frame refuses a token minted for a different session. Scenes saved during the session are readable afterwards through the game-saves endpoints. REQUIRES a delegated token — the clinician is read from the token, not the body. An unknown patient id, one belonging to another organization, and one outside the acting clinician's roster all answer 404.
      tags:
        - Games
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Which scene to run, for which patient, and which origins may frame it.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerGameSessionCreateBody"
      responses:
        "201":
          description: The session is live and both seats can join. `Location` names the session, which has no read endpoint of its own — the two seats are the resource.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerGameSessionResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/health:
    get:
      operationId: getPartnerApiHealth
      summary: Partner API health check
      description: Lightweight authenticated probe. Returns 200 with the partner organization id and a server timestamp whenever the request token is accepted. Always-accessible — no scopes required.
      tags:
        - Meta
      responses:
        "200":
          description: The partner token authenticated successfully.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/notes:
    get:
      operationId: listPartnerNotes
      summary: List the acting clinician's notes
      description: "Returns the clinical notes written by the clinician the delegated token is acting as, newest first. Each note carries the appointment, patient and practitioner it belongs to as ids on your own records, so you can file it against the encounter you booked. Bodies are omitted by default — add `include=content` to get every listed note's written body and version number in the same response, in one request rather than one per note. A note taken during an ad-hoc recording, rather than during an appointment you booked, still appears here with null appointment and patient ids. REQUIRES a delegated token: an organization-wide token has no clinician to scope to and is refused. This surface is read-only — notes cannot be created, changed or removed through this API."
      tags:
        - Notes
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include
          schema:
            description: "Set to `content` to get every listed note's written body and version number in the same response. Omitted by default: a page of notes returns titles and timing so you can decide what to open, and the bodies are a much larger release of clinical information than the list itself. One request either way — never one per note."
            example: content
            type: string
            const: content
          description: "Set to `content` to get every listed note's written body and version number in the same response. Omitted by default: a page of notes returns titles and timing so you can decide what to open, and the bodies are a much larger release of clinical information than the list itself. One request either way — never one per note."
      responses:
        "200":
          description: The clinician's notes, newest first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerNoteListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/notes/{id}:
    get:
      operationId: getPartnerNote
      summary: Get a note by id
      description: "Returns one clinical note written by the clinician the delegated token is acting as, always including its written body and version number — a note read one at a time is the case where the body is the answer. Add `include=transcript` to get the verbatim, speaker-diarised recording of the session alongside it; that is available here and on no other endpoint, because a transcript is every word both people said rather than what the clinician chose to record, and a long session runs to megabytes. Treat the whole payload as clinical content: render it, do not log it. A note belonging to another clinician is indistinguishable from one that never existed — both return 404. REQUIRES a delegated token."
      tags:
        - Notes
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the note, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the note, as returned on the list response as `data[].id`.
        - in: query
          name: include
          schema:
            description: "Set to `transcript` to get the verbatim, speaker-diarised recording of the session alongside the clinician's written note. Omitted by default, and available on this endpoint only: a transcript is every word both people said rather than what the clinician chose to record, and a long session runs to megabytes. Ask for it one note at a time, when you have a reason to."
            example: transcript
            type: string
            const: transcript
          description: "Set to `transcript` to get the verbatim, speaker-diarised recording of the session alongside the clinician's written note. Omitted by default, and available on this endpoint only: a transcript is every word both people said rather than what the clinician chose to record, and a long session runs to megabytes. Ask for it one note at a time, when you have a reason to."
      responses:
        "200":
          description: The note at the requested id, with its written body, and its transcript when `include=transcript` was set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerNoteDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/patients:
    get:
      operationId: listPartnerPatients
      summary: List patients
      description: List your patients across your clinics. Use `partner_clinic_id` to narrow to one clinic, `partner_practitioner_id` to narrow to patients assigned to one practitioner, or both together to scope to one practitioner within one clinic. Supports pagination and an `include_deleted` toggle. Requires the `patients:read` scope.
      tags:
        - Patients
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: partner_clinic_id
          schema:
            format: uuid
            description: Optional filter. Return only patients belonging to this clinic. When omitted, returns patients across every clinic you can access.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Optional filter. Return only patients belonging to this clinic. When omitted, returns patients across every clinic you can access.
        - in: query
          name: partner_practitioner_id
          schema:
            format: uuid
            description: Optional filter. Return only patients assigned to this practitioner. May be combined with `partner_clinic_id` to scope to one practitioner within one clinic. When omitted, returns patients across every practitioner you can access.
            example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Optional filter. Return only patients assigned to this practitioner. May be combined with `partner_clinic_id` to scope to one practitioner within one clinic. When omitted, returns patients across every practitioner you can access.
        - in: query
          name: include_deleted
          schema:
            description: When true, include patients that have been removed from the roster. Defaults to false. Accepts "true"/"1"/"on" as true and "false"/"0"/"off"/"" or omission as false; any other value is a 422.
            example: false
            type: boolean
          description: When true, include patients that have been removed from the roster. Defaults to false. Accepts "true"/"1"/"on" as true and "false"/"0"/"off"/"" or omission as false; any other value is a 422.
      responses:
        "200":
          description: A page of patient records.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPatientListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerPatient
      summary: Create a patient
      description: Creates a patient. The new patient is anchored to `partner_clinic_id` and credited to `created_by_partner_practitioner_id`, which also becomes the patient's first linked practitioner. Both ids must reference your own active clinic and practitioner; the practitioner may be in any clinic, not only `partner_clinic_id`. An invalid or unknown value for either id is a 422.  Email is optional and has no uniqueness check (see the Patients tag); a duplicate email succeeds. The 201 response carries the new patient record and a `Location` header pointing at it.  On a delegated token, `created_by_partner_practitioner_id` must equal the token's clinician, otherwise a 422.  Requires the `patients:write` scope.
      tags:
        - Patients
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Identity and contact fields for the new patient, plus the `partner_clinic_id` anchor and the `created_by_partner_practitioner_id` creator. See PartnerPatientCreateBody.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPatientCreateBody"
      responses:
        "201":
          description: The patient was created. The Location header points at the new resource.
          headers:
            Location:
              description: Canonical URL of the new patient — /v1/partner/patients/{id}.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPatientCreateResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `patients:write` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error. The body failed validation (unknown key, blank name, malformed date, or invalid timezone), the Idempotency-Key was missing, or `partner_clinic_id` / `created_by_partner_practitioner_id` did not reference one of your active records. There is no 409 on this endpoint.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/patients/{id}:
    get:
      operationId: getPartnerPatient
      summary: Get a patient by id
      description: Returns a single patient by id (the same id surfaced on LIST responses as `data[].id`).  Pass `?include_linked_practitioners=true` to add a `linked_partner_practitioner_ids` field listing the practitioners currently linked to this patient. It defaults to false and is omitted otherwise.  Requires the `patients:read` scope.
      tags:
        - Patients
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the patient, as returned on LIST responses as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the patient, as returned on LIST responses as `data[].id`.
        - in: query
          name: include_linked_practitioners
          schema:
            description: When true, add a `linked_partner_practitioner_ids` field to the response listing the practitioners currently linked to this patient. Defaults to false, in which case the field is omitted. Accepts "true"/"1"/"on" as true and "false"/"0"/"off"/"" or omission as false; any other value is a 422.
            example: false
            type: boolean
          description: When true, add a `linked_partner_practitioner_ids` field to the response listing the practitioners currently linked to this patient. Defaults to false, in which case the field is omitted. Accepts "true"/"1"/"on" as true and "false"/"0"/"off"/"" or omission as false; any other value is a 422.
      responses:
        "200":
          description: The patient record at the requested id. Carries `linked_partner_practitioner_ids` only when `include_linked_practitioners=true`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPatientResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No patient exists at this id within your scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerPatient
      summary: Update a patient
      description: "Partially updates a patient. Accepts any subset of the mutable fields — `first_name`, `last_name`, `preferred_name`, `date_of_birth`, `email`, `phone_number`, `timezone` — and at least one is required. `partner_clinic_id` and `created_by_partner_practitioner_id` are not accepted; both are fixed at creation.  A PATCH whose supplied values all equal the current values returns 200 with the current record. Email is mutable and has no uniqueness check (see the Patients tag): setting any value, including another patient's email, succeeds, though a malformed email is still a 422.  The 200 response carries the updated patient record without the `linked_partner_practitioner_ids` field.  A delegated token may update only patients on its clinician's roster; any other id returns a 404.  Requires the `patients:write` scope."
      tags:
        - Patients
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the patient, as returned on LIST responses as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the patient, as returned on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Partial-update body. At least one mutable field is required. See PartnerPatientUpdateBody.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPatientUpdateBody"
      responses:
        "200":
          description: The patient was updated. A no-op PATCH whose supplied values all equal the current values also returns 200 with the current record.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPatientUpdateResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `patients:write` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No patient exists at this id within your scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: The Idempotency-Key has an in-flight original call still running. Retriable once it settles or with a fresh key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error. The body failed validation (no field supplied, a blank name, a malformed date, email, or timezone, or an unknown key), the path `id` was not a UUID, the Idempotency-Key was missing, or the Idempotency-Key was reused with a different body.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerPatient
      summary: Soft-delete a patient
      description: "Removes the patient from your roster. This is a soft delete: the patient drops out of the default LIST (surface it again with `include_deleted=true`) and GET-by-id and a subsequent PATCH or DELETE on the same id return 404. A repeated DELETE on the same id is therefore a 404, not an error.  A patient with future appointments can still be deleted in this version; those appointments keep referencing the patient.  Deleting a patient is an organization-level action: it requires an organization token, and a delegated token receives a 403.  Requires the `patients:delete` scope."
      tags:
        - Patients
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the patient, as returned on LIST responses as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the patient, as returned on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The patient was removed from the roster. No response body. Subsequent GET-by-id returns 404, and LIST omits the row unless `include_deleted=true`.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: The token does not grant the `patients:delete` scope.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No patient exists at this id within your scope. A second DELETE on an already-removed id returns the same 404.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: The Idempotency-Key has an in-flight original call still running. Retriable once it settles or with a fresh key.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error. The path `id` was not a UUID, the Idempotency-Key was missing, or the Idempotency-Key was reused with a different id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/playrooms:
    get:
      operationId: listPartnerPlayrooms
      summary: List the acting practitioner's playrooms
      description: Returns the playrooms owned by the practitioner the delegated token is acting as, newest first, including the starter rooms created with the account (`is_default`). Supports cursor pagination, a `published` filter, and an `include_deleted` toggle. REQUIRES a delegated token — an organization token has no practitioner to scope to and is refused.
      tags:
        - Playrooms
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include_deleted
          schema:
            description: When true, removed rooms are included. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, removed rooms are included. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
        - in: query
          name: published
          schema:
            description: "Filter by publication state: `true` returns only rooms offered at session start, `false` only drafts. Omit to return both."
            example: true
            type: boolean
          description: "Filter by publication state: `true` returns only rooms offered at session start, `false` only drafts. Omit to return both."
      responses:
        "200":
          description: A page of the practitioner's playrooms. `meta.pagination.next_cursor` is null once the list is fully consumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayroomListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerPlayroom
      summary: Create a playroom for the acting practitioner
      description: Creates a playroom owned by the practitioner the delegated token is acting as. Only `title` is required; `room_type`, `color_palette`, `session_items`, `tags`, and `published` fall back to the same defaults the PlaySpace app applies, and the room thumbnail is derived from `room_type` and `color_palette`. Titles are not unique — two rooms may share one, so there is no conflict response for a duplicate name. The 201 body is the same `PartnerPlayroomResponse` envelope returned by `GET /v1/partner/playrooms/{id}`, and the `Location` header points at the new room. REQUIRES a delegated token — an organization token is refused, because a room must belong to one clinician.
      tags:
        - Playrooms
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The new room. Only `title` is required. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPlayroomCreateBody"
      responses:
        "201":
          description: The room was created. The body matches the `GET/{id}` `PartnerPlayroomResponse` envelope, and the `Location` header points at the new room.
          headers:
            Location:
              description: Canonical URL of the new room — `/v1/partner/playrooms/{id}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayroomResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerPlayroomCreateBody` (unknown key, blank title, or an unrecognised session item), the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/playrooms/{id}:
    get:
      operationId: getPartnerPlayroom
      summary: Get a playroom by id
      description: Returns a single playroom by id — the value surfaced on LIST responses as `data[].id`. A removed room is returned only when `include_deleted=true`; otherwise it 404s, as does a room belonging to any other practitioner. REQUIRES a delegated token.
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, the room is returned even if it has been removed. When false (the default), a removed room returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, the room is returned even if it has been removed. When false (the default), a removed room returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
      responses:
        "200":
          description: The room at the requested id. When `include_deleted=true` and the room has been removed, `data.deleted_at` carries the timestamp.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayroomResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerPlayroom
      summary: Update a playroom
      description: Partially updates a playroom. Accepts any subset of the create fields; at least one is required. `session_items` and `tags` replace the current arrays wholesale rather than merging. Changing `room_type` or `color_palette` re-derives the thumbnail. A PATCH whose every supplied value already equals the current value is a 200 no-op that leaves `updated_at` unchanged. A room belonging to another practitioner, or one that has been removed, returns 404. The 200 body is the same `PartnerPlayroomResponse` envelope returned by `GET /v1/partner/playrooms/{id}`. REQUIRES a delegated token.
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Partial-update body. At least one field is required. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPlayroomUpdateBody"
      responses:
        "200":
          description: The room was updated. The body matches the `GET/{id}` `PartnerPlayroomResponse` envelope. A no-op PATCH also returns 200, with `updated_at` unchanged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPlayroomResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerPlayroomUpdateBody` (empty body, unknown key, blank title, or an unrecognised session item), the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/playrooms/{id}/content:
    get:
      operationId: listPartnerPlayroomContent
      summary: List the content on a playroom's shelf
      description: "Returns every worksheet and storybook attached to this playroom's activity shelf. Items are ordered by `order_index` where the pairing carries one and by attachment time otherwise, and the whole shelf comes back in one response — there is no pagination. The response identifies the content rather than embedding it: read a worksheet or a storybook through its own endpoint. A playroom belonging to another practitioner, or one that has been removed, returns 404. Requires the `practitioners:read` scope, and REQUIRES a delegated token — a playroom belongs to one clinician, so an organization token has no practitioner to scope to and is refused."
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
      responses:
        "200":
          description: The playroom's shelf. An empty `data` array means the playroom has no content on it, which is a legitimate state rather than an error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/playrooms/{id}/content/{contentType}/{contentId}:
    put:
      operationId: attachPartnerPlayroomContent
      summary: Attach a worksheet or storybook to a playroom
      description: "Puts one worksheet or storybook on this playroom's activity shelf. The attachment is identified entirely by its URL, so the request takes no body and repeating it is safe: the first call returns 201 with a `Location` header, and every later call for the same pair returns 200 with no `Location` and changes nothing. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. Content that exists and is yours but is not in a shelf-renderable published state is refused with 422 and `reason: not_shelf_ready` — check `shelf_ready` on the content lists first. An unknown playroom or content id returns 404. Requires the `practitioners:write` scope, and REQUIRES a delegated token."
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "200":
          description: The content was already on the shelf, so nothing changed. The body is the existing attachment and there is no `Location` header — that is how a caller tells a re-attach from a first attach.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "201":
          description: The content was put on the shelf. The `Location` header points at the new attachment.
          headers:
            Location:
              description: Canonical URL of the attachment — `/v1/partner/playrooms/{id}/content/{contentType}/{contentId}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error, in one of two kinds. Either the request could not be understood — an unrecognised `contentType` path segment or a malformed id (`source` is `path`), or a missing or reused `Idempotency-Key`. Or the content exists and is yours but is not shelf-ready: its publication state is not the one the in-session shelf renders, so attaching it would create a shelf entry no session participant can see. That second kind carries `reason: "not_shelf_ready"` with `source: "path"`; publish the content and retry. Use `shelf_ready` on the content list responses to know in advance which ids an attach will accept.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PartnerContentAttachProblem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    get:
      operationId: getPartnerPlayroomContent
      summary: Get one content attachment on a playroom
      description: Returns the shelf entry for one worksheet or storybook on this playroom — the same object the collection endpoint returns, addressed directly, so an integration can confirm one attachment without re-reading the whole shelf. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. If the playroom or the content is unknown, belongs to another clinician or has been removed, or the content is simply not on the shelf, the answer is one identical 404 that never says which. Requires the `practitioners:read` scope, and REQUIRES a delegated token.
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
      responses:
        "200":
          description: The attachment at the requested playroom, content type, and content id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error — the `contentType` path segment is not one of the recognised content types, or an id in the path is not a UUID. The problem body carries `source: "path"`. On the detach verb this status is also returned when the `Idempotency-Key` header is missing or was reused with a different request.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: detachPartnerPlayroomContent
      summary: Detach content from a playroom
      description: Takes one worksheet or storybook off this playroom's activity shelf. The content itself is untouched and stays in the clinician's library; only the shelf entry goes. Detaching is idempotent — a pair that is already off the shelf still returns 204, so a retry after a dropped connection is safe. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. A playroom or a content id that is unknown, belongs to another clinician, or has been removed returns 404. Requires the `practitioners:write` scope, and REQUIRES a delegated token.
      tags:
        - Playrooms
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the playroom, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The content is no longer on the shelf. No response body. Returned whether the call removed the attachment or there was nothing to remove.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error — the `contentType` path segment is not one of the recognised content types, or an id in the path is not a UUID. The problem body carries `source: "path"`. On the detach verb this status is also returned when the `Idempotency-Key` header is missing or was reused with a different request.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/practitioners:
    get:
      operationId: listPartnerPractitioners
      summary: List partner practitioners
      description: Lists your practitioners, newest first. Filter to one clinic with `partner_clinic_id`, and set `include_deleted=true` to include removed practitioners. Requires the `practitioners:read` scope.
      tags:
        - Practitioners
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: partner_clinic_id
          schema:
            format: uuid
            description: Return only practitioners belonging to this clinic. When omitted, returns practitioners across all clinics you can access.
            example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          description: Return only practitioners belonging to this clinic. When omitted, returns practitioners across all clinics you can access.
        - in: query
          name: include_deleted
          schema:
            description: When `true`, include removed practitioners in the results. Defaults to `false`.
            example: false
            type: boolean
          description: When `true`, include removed practitioners in the results. Defaults to `false`.
      responses:
        "200":
          description: A page of practitioners.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPractitionerListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerPractitioner
      summary: Create a partner practitioner
      description: "Creates a practitioner in the clinic named by `partner_clinic_id`. The supplied email becomes the practitioner login, so an email already in use by another practitioner returns 409. A `partner_clinic_id` you cannot access returns 422 naming that field. On success the 201 body matches the get-by-id response and the `Location` header points at the new record. Retries follow the standard idempotency contract. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `practitioners:write` scope."
      tags:
        - Practitioners
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Identity fields and the clinic for the new practitioner.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPractitionerCreateBody"
      responses:
        "201":
          description: The practitioner was created. The body matches the get-by-id response, and the `Location` header points at the new record.
          headers:
            Location:
              description: URL of the new practitioner.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPractitionerResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: A conflict. Inspect the body's `type` to distinguish `partner-email-conflict` (the email is already in use by another practitioner) from `idempotency-conflict`.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation. The `source` extension or problem `type` identifies the cause, including a `partner_clinic_id` that does not reference a clinic you can access.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/practitioners/{id}:
    get:
      operationId: getPartnerPractitioner
      summary: Get a partner practitioner by id
      description: Returns a single practitioner by id. A removed practitioner returns 404 unless you set `include_deleted=true`. Requires the `practitioners:read` scope.
      tags:
        - Practitioners
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
            example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
        - in: query
          name: include_deleted
          schema:
            description: When `true`, return the practitioner even if it has been removed. When `false` (the default), a removed practitioner returns 404.
            example: false
            type: boolean
          description: When `true`, return the practitioner even if it has been removed. When `false` (the default), a removed practitioner returns 404.
      responses:
        "200":
          description: The requested practitioner. For a removed practitioner returned via `include_deleted=true`, `data.deleted_at` carries the removal timestamp.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPractitionerResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerPractitioner
      summary: Update a partner practitioner
      description: "Updates a practitioner with any subset of `first_name`, `last_name`, `email`, `role`, and `status`; at least one is required. Setting `status` to `disabled` suspends the practitioner's per-clinician (delegated) access without removing any records; setting it back to `active` restores access — the reversible alternative to removal. A PATCH whose supplied values all already match the current record is a no-op and returns 200 with the current record. Changing the email to one already in use by another practitioner returns 409, while setting it to the practitioner's own current email is a no-op. A practitioner cannot be moved to a different clinic. On success the 200 body matches the get-by-id response. Retries follow the standard idempotency contract. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `practitioners:write` scope."
      tags:
        - Practitioners
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
            example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Fields to change. At least one is required.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerPractitionerUpdateBody"
      responses:
        "200":
          description: The updated practitioner, or the unchanged practitioner when the PATCH was a no-op. The body matches the get-by-id response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerPractitionerResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No practitioner with this id is available to update.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: "A conflict. Inspect the body's `type` to distinguish `partner-email-conflict` (the email is already in use by another practitioner), `practitioner-owner-protected` (the practitioner owns a clinic that has moved to direct PlaySpace logins, so disabling them would lock that clinic's administrator out: reassign the owner role first), and `idempotency-conflict`."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation. The `source` extension or problem `type` identifies the cause.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerPractitioner
      summary: Soft-delete a partner practitioner
      description: "Removes the practitioner from your roster. Afterward, get-by-id returns 404 and list omits the practitioner unless `include_deleted=true`. A practitioner with future appointments can still be removed, and those appointments keep referencing the same clinician. Retries follow the standard idempotency contract. This is an organization-level action: it requires an organization token, and a delegated token receives a 403. Requires the `practitioners:delete` scope."
      tags:
        - Practitioners
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
            example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
          required: true
          description: Identifier of the practitioner, as returned in the `id` field of list and single-record responses.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The practitioner was removed. No response body. Later reads return 404 unless `include_deleted=true`.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: No practitioner with this id is available to remove. A repeat delete of an already-removed practitioner returns this same response.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: "A conflict. Inspect the body's `type`. `practitioner-owner-protected`: the practitioner owns a clinic that has moved to direct PlaySpace logins, so removing them would lock that clinic's administrator out; reassign the owner role first, then retry. `idempotency-conflict`: retriable once the original call settles or with a fresh key."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation. The `source` extension or problem `type` identifies the cause.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/session-config:
    get:
      operationId: getPartnerSessionConfig
      summary: Read your organization's session configuration
      description: "Returns the configuration your PlaySpace sessions and framed workspace run on, including `partner_editable_fields` — the list of fields you may change. An organization PlaySpace has not configured yet gets the defaults it is actually running on rather than an empty response, with an empty `partner_editable_fields` meaning every field is locked. Read this before and after a change: it is the only way to confirm what a change did."
      tags:
        - Session configuration
      responses:
        "200":
          description: Your organization's configuration as it stands now.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerSessionConfigResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerSessionConfig
      summary: Change your organization's session configuration
      description: Changes one or more configuration fields. Only the fields named in `partner_editable_fields` may be changed; any other field is refused with 403 and NOTHING in the request is applied, so a request that mixes an allowed field with a locked one leaves the configuration exactly as it was. The 403 names the fields it refused. `partner_editable_fields` and `is_active` cannot be changed through this API at all and are rejected as unknown fields. Turning a feature off in `enabled_features` removes it from the framed workspace for every clinician in your organization; turning one on cannot grant a feature your organization is not entitled to.
      tags:
        - Session configuration
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerSessionConfigPatch"
      responses:
        "200":
          description: The configuration after the change.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerSessionConfigResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks:
    get:
      operationId: listPartnerStorybooks
      summary: List the acting practitioner's storybooks
      description: "Returns storybooks owned by the practitioner the delegated token is acting as, newest first. Deleted books are omitted unless `include_deleted=true`, which returns them with a `deleted_at` timestamp. Add `include=pages` to get every book's pages and image links in the same response instead of one request per book. REQUIRES a delegated token — an org-wide admin token has no practitioner to scope to and is refused. Pair with an embedded reader to open a book: the same embed token that lists here can render any of these ids."
      tags:
        - Storybooks
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include_deleted
          schema:
            description: When true, deleted storybooks are included, each carrying the `deleted_at` timestamp. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, deleted storybooks are included, each carrying the `deleted_at` timestamp. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
        - in: query
          name: include
          schema:
            description: Set to `pages` to get every listed book's pages, with their text and image links, in the same response. One request instead of one per book.
            example: pages
            type: string
            const: pages
          description: Set to `pages` to get every listed book's pages, with their text and image links, in the same response. One request instead of one per book.
      responses:
        "200":
          description: The practitioner's storybooks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerStorybook
      summary: Generate a storybook
      description: "Starts generating an illustrated storybook for the acting practitioner and returns it straight away with status `generating` and no pages yet. Illustration takes a minute or two — poll the returned id until the status is `ready` (or `failed`). Generation costs money and cannot be cancelled once accepted, so send a fresh Idempotency-Key per book and reuse that key on any retry: a repeated key returns the original book instead of generating a second one. The practice must have storybook generation enabled. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: What the story should be about, and how it should read and look.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerStorybookCreateBody"
      responses:
        "201":
          description: Generation has started. The body is the same shape as a get-by-id, with an empty page list. `Location` points at the book.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}:
    get:
      operationId: getPartnerStorybook
      summary: Get a storybook by id
      description: "Returns one storybook belonging to the practitioner the delegated token is acting as, with its pages in reading order. Page text is the story written for a child and page images are short-lived signed links, so treat the whole payload as clinical content: render it, do not log it. A book that is still generating returns with an empty `pages` array. A deleted book is a 404 unless `include_deleted=true`. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The storybook at the requested id, with its pages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerStorybook
      summary: Make a storybook shelf-ready
      description: "Makes a storybook eligible to be placed on a playroom or toolkit shelf, and returns the book with `shelf_ready` now true. This is the step between generating a book and attaching it: a book that is not shelf-ready is refused by the attach endpoints with `reason: not_shelf_ready`. Publishing a book that is still generating is allowed — it becomes attachable straight away and fills in as it finishes — so check `status` first if you want a finished book on the shelf. Send it again on a book that is already shelf-ready and nothing changes. PRECONDITION: only a book still in the practitioner's own library — a draft, or one already on a playroom or toolkit shelf — can be made shelf-ready. A book they shared to the community or to their clinic, and a client's own copy, answer 404 here even though the list and get-by-id both return them. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The change to apply.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerStorybookUpdateBody"
      responses:
        "200":
          description: The storybook after the change, in the same shape as a get-by-id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — no book at this id can be made shelf-ready. Either nothing exists at this id for the practitioner you are acting as, or the book exists but is not in their library any more: one shared out to the community or to a clinic, and a client's own copy, are both refused here. A get-by-id on the same id can still return 200."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerStorybook
      summary: Delete a storybook
      description: "Removes a storybook from the acting practitioner's library. This is a soft delete: the book drops out of the list, any copy already assigned to a client is detached rather than removed, and its stored images are cleared. A repeated delete on the same id returns 404, not an error. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The storybook was deleted. No response body.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/pages:
    get:
      operationId: listPartnerStorybookPages
      summary: List a storybook's pages
      description: "Returns every page of the book in reading order, with its text and a link to its illustration. Not paginated — a book is small. Useful on its own because the image links expire after an hour: re-read this to refresh them without pulling the whole book. Page text is the story written for a child, so treat the response as clinical content. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The pages of the storybook, in reading order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookPagesResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/pages/{pageId}:
    get:
      operationId: getPartnerStorybookPage
      summary: Get one page of a storybook
      description: Returns a single page — its position in the book, its text, and a link to its illustration. A page id that belongs to a different book returns 404, the same as one that does not exist. REQUIRES a delegated token.
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook the page belongs to.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook the page belongs to.
        - in: path
          name: pageId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the page, as returned on the pages response as `data[].id`.
            example: 9f8e7d6c-5b4a-4392-8172-6f5e4d3c2b1a
          required: true
          description: ID of the page, as returned on the pages response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The requested page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookPageResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/pages/{pageId}/image:
    get:
      operationId: downloadPartnerStorybookPageImage
      summary: Download a page's illustration
      description: "Returns the illustration itself, as image bytes rather than a link. Use this when you need to store or re-serve the artwork: the `image_url` on the read endpoints is a temporary link that stops working after an hour, while this endpoint keeps working for as long as the book exists. A page with no illustration yet returns 404. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook the page belongs to.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook the page belongs to.
        - in: path
          name: pageId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the page, as returned on the pages response as `data[].id`.
            example: 9f8e7d6c-5b4a-4392-8172-6f5e4d3c2b1a
          required: true
          description: ID of the page, as returned on the pages response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The illustration. The content type is whatever the image was stored as.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/cover:
    get:
      operationId: downloadPartnerStorybookCover
      summary: Download a storybook's cover image
      description: Returns the cover image itself, as image bytes rather than a link — the counterpart to the page image endpoint, and the durable alternative to the temporary `thumbnail_url` on the read endpoints. A book with no cover rendered yet returns 404. REQUIRES a delegated token.
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The cover image. The content type is whatever the image was stored as.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/download:
    get:
      operationId: downloadPartnerStorybook
      summary: Download a storybook as a PDF
      description: Returns the storybook as a PDF — a cover page followed by one page per story page, the same file the clinician downloads in PlaySpace. The response body is the PDF itself, not a JSON envelope, and the filename is carried on `Content-Disposition`. A book with no pages yet (still generating, or generation failed) returns 422 rather than an empty file. Rendering fetches every illustration, so this call is slower than the other reads. REQUIRES a delegated token.
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted storybook is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The storybook as a PDF document.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/storybooks/{id}/share:
    post:
      operationId: sharePartnerStorybook
      summary: Email a storybook to a patient
      description: "Emails the patient a secure link to read the storybook, valid for the requested window. The address is taken from the patient's own record — there is no recipient field, so a book can only ever reach the patient you name, and that patient must be on the acting practitioner's current roster; a patient with no email on file is a 422. The book must be ready: one still generating, or whose generation failed, is a 422 rather than a dead link. Re-sending to the same patient extends the existing link instead of issuing a second one. Send a fresh Idempotency-Key per send: a repeated key returns the original result WITHOUT sending again. REQUIRES a delegated token."
      tags:
        - Storybooks
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the storybook, as returned on the list response as `data[].id`.
            example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
          required: true
          description: ID of the storybook, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Which patient to send to, optionally how long the link should last, and an optional note for the email.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerContentShareBody"
      responses:
        "200":
          description: The email was accepted for delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerStorybookShareResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/toolkits:
    get:
      operationId: listPartnerToolkits
      summary: List the acting practitioner's toolkits
      description: Returns the toolkits owned by the practitioner the delegated token is acting as, newest first, including the starter toolkits created with the account (`is_default`). Supports cursor pagination, a `published` filter, and an `include_deleted` toggle. REQUIRES a delegated token — an organization token has no practitioner to scope to and is refused.
      tags:
        - Toolkits
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include_deleted
          schema:
            description: When true, removed toolkits are included. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, removed toolkits are included. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
        - in: query
          name: published
          schema:
            description: "Filter by publication state: `true` returns only toolkits offered at session start, `false` only drafts. Omit to return both."
            example: true
            type: boolean
          description: "Filter by publication state: `true` returns only toolkits offered at session start, `false` only drafts. Omit to return both."
      responses:
        "200":
          description: A page of the practitioner's toolkits. `meta.pagination.next_cursor` is null once the list is fully consumed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerToolkitListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerToolkit
      summary: Create a toolkit for the acting practitioner
      description: Creates a toolkit owned by the practitioner the delegated token is acting as. Only `title` is required; `session_items`, `tags`, and `published` fall back to the same defaults the PlaySpace app applies. Titles are not unique — two toolkits may share one, so there is no conflict response for a duplicate name. The 201 body is the same `PartnerToolkitResponse` envelope returned by `GET /v1/partner/toolkits/{id}`, and the `Location` header points at the new toolkit. REQUIRES a delegated token — an organization token is refused, because a toolkit must belong to one clinician.
      tags:
        - Toolkits
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The new toolkit. Only `title` is required. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerToolkitCreateBody"
      responses:
        "201":
          description: The toolkit was created. The body matches the `GET/{id}` `PartnerToolkitResponse` envelope, and the `Location` header points at the new toolkit.
          headers:
            Location:
              description: Canonical URL of the new toolkit — `/v1/partner/toolkits/{id}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerToolkitResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerToolkitCreateBody` (unknown key, blank title, or an unrecognised session item), the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/toolkits/{id}:
    get:
      operationId: getPartnerToolkit
      summary: Get a toolkit by id
      description: Returns a single toolkit by id — the value surfaced on LIST responses as `data[].id`. A removed toolkit is returned only when `include_deleted=true`; otherwise it 404s, as does a toolkit belonging to any other practitioner. REQUIRES a delegated token.
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, the toolkit is returned even if it has been removed. When false (the default), a removed toolkit returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, the toolkit is returned even if it has been removed. When false (the default), a removed toolkit returns 404. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
      responses:
        "200":
          description: The toolkit at the requested id. When `include_deleted=true` and the toolkit has been removed, `data.deleted_at` carries the timestamp.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerToolkitResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    patch:
      operationId: updatePartnerToolkit
      summary: Update a toolkit
      description: Partially updates a toolkit. Accepts any subset of the create fields; at least one is required. `session_items` and `tags` replace the current arrays wholesale rather than merging. A PATCH whose every supplied value already equals the current value is a 200 no-op that leaves `updated_at` unchanged. A toolkit belonging to another practitioner, or one that has been removed, returns 404. The 200 body is the same `PartnerToolkitResponse` envelope returned by `GET /v1/partner/toolkits/{id}`. REQUIRES a delegated token.
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Partial-update body. At least one field is required. Unknown keys are rejected.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerToolkitUpdateBody"
      responses:
        "200":
          description: The toolkit was updated. The body matches the `GET/{id}` `PartnerToolkitResponse` envelope. A no-op PATCH also returns 200, with `updated_at` unchanged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerToolkitResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: The request failed validation — the body did not match `PartnerToolkitUpdateBody` (empty body, unknown key, blank title, or an unrecognised session item), the `Idempotency-Key` header was missing, or the key was reused with a different body (`idempotency-key-mismatch`). The `source` extension and problem `type` slug identify which.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/toolkits/{id}/content:
    get:
      operationId: listPartnerToolkitContent
      summary: List the content in a toolkit
      description: "Returns every worksheet and storybook attached to this toolkit. Items are ordered by `order_index` where the pairing carries one and by attachment time otherwise, and the whole toolkit comes back in one response — there is no pagination. The response identifies the content rather than embedding it: read a worksheet or a storybook through its own endpoint. A toolkit belonging to another practitioner, or one that has been removed, returns 404. Requires the `practitioners:read` scope, and REQUIRES a delegated token — a toolkit belongs to one clinician, so an organization token has no practitioner to scope to and is refused."
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
      responses:
        "200":
          description: The toolkit's content. An empty `data` array means the toolkit holds nothing yet, which is a legitimate state rather than an error.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/toolkits/{id}/content/{contentType}/{contentId}:
    put:
      operationId: attachPartnerToolkitContent
      summary: Attach a worksheet or storybook to a toolkit
      description: "Puts one worksheet or storybook into this toolkit. The attachment is identified entirely by its URL, so the request takes no body and repeating it is safe: the first call returns 201 with a `Location` header, and every later call for the same pair returns 200 with no `Location` and changes nothing. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. Content that exists and is yours but is not in a shelf-renderable published state is refused with 422 and `reason: not_shelf_ready` — check `shelf_ready` on the content lists first. An unknown toolkit or content id returns 404. Requires the `practitioners:write` scope, and REQUIRES a delegated token."
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "200":
          description: The content was already in the toolkit, so nothing changed. The body is the existing attachment and there is no `Location` header — that is how a caller tells a re-attach from a first attach.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "201":
          description: The content was added to the toolkit. The `Location` header points at the new attachment.
          headers:
            Location:
              description: Canonical URL of the attachment — `/v1/partner/toolkits/{id}/content/{contentType}/{contentId}`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error, in one of two kinds. Either the request could not be understood — an unrecognised `contentType` path segment or a malformed id (`source` is `path`), or a missing or reused `Idempotency-Key`. Or the content exists and is yours but is not shelf-ready: its publication state is not the one the in-session shelf renders, so attaching it would create a shelf entry no session participant can see. That second kind carries `reason: "not_shelf_ready"` with `source: "path"`; publish the content and retry. Use `shelf_ready` on the content list responses to know in advance which ids an attach will accept.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/PartnerContentAttachProblem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    get:
      operationId: getPartnerToolkitContent
      summary: Get one content attachment in a toolkit
      description: Returns the entry for one worksheet or storybook in this toolkit — the same object the collection endpoint returns, addressed directly, so an integration can confirm one attachment without re-reading the whole toolkit. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. If the toolkit or the content is unknown, belongs to another clinician or has been removed, or the content is simply not in the toolkit, the answer is one identical 404 that never says which. Requires the `practitioners:read` scope, and REQUIRES a delegated token.
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
      responses:
        "200":
          description: The attachment at the requested toolkit, content type, and content id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerContentItemResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error — the `contentType` path segment is not one of the recognised content types, or an id in the path is not a UUID. The problem body carries `source: "path"`. On the detach verb this status is also returned when the `Idempotency-Key` header is missing or was reused with a different request.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: detachPartnerToolkitContent
      summary: Detach content from a toolkit
      description: Takes one worksheet or storybook out of this toolkit. The content itself is untouched and stays in the clinician's library; only the attachment goes. Detaching is idempotent — a pair that is already out of the toolkit still returns 204, so a retry after a dropped connection is safe. `contentType` is `worksheet` or `storybook`; anything else is a 422 naming `path` as the source. A toolkit or a content id that is unknown, belongs to another clinician, or has been removed returns 404. Requires the `practitioners:write` scope, and REQUIRES a delegated token.
      tags:
        - Toolkits
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
            example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
          required: true
          description: UUID of the toolkit, as surfaced on LIST responses as `data[].id`.
        - in: path
          name: contentType
          schema:
            $ref: "#/components/schemas/PartnerContentType"
          required: true
          description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
        - in: path
          name: contentId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
            example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          required: true
          description: UUID of the worksheet or storybook, as returned by `GET /v1/partner/worksheets` or `GET /v1/partner/storybooks`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The content is no longer in the toolkit. No response body. Returned whether the call removed the attachment or there was nothing to remove.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: "Not Found — one identical response for every reason the request cannot be satisfied: the playroom or toolkit is unknown, belongs to another clinician, or has been removed; the worksheet, storybook or form is unknown, belongs to another clinician, or has been removed; or the attachment does not exist. The body never says which, and never echoes an id, so ids cannot be probed by trying them."
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: 'Validation Error — the `contentType` path segment is not one of the recognised content types, or an id in the path is not a UUID. The problem body carries `source: "path"`. On the detach verb this status is also returned when the `Idempotency-Key` header is missing or was reused with a different request.'
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets:
    get:
      operationId: listPartnerWorksheets
      summary: List the acting practitioner's worksheets
      description: "Returns the worksheet library of the practitioner the delegated token is acting as, newest first. Deleted worksheets are omitted unless `include_deleted=true`, which returns them with a `deleted_at` timestamp. Add `include=pages` to get every worksheet's pages and image links in the same response instead of one request per worksheet. REQUIRES a delegated token — an org-wide admin token has no practitioner to scope to and is refused. Pair with an embedded viewer to open a worksheet: the same embed token that lists here can render any of these ids. There is no endpoint for creating a worksheet — a worksheet is made by converting a PDF inside an embedded upload surface."
      tags:
        - Worksheets
      parameters:
        - in: query
          name: cursor
          schema:
            description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
            type: string
          description: Opaque cursor returned by the previous page as `meta.pagination.next_cursor`. Omit on the first request.
        - in: query
          name: limit
          schema:
            default: 25
            description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
            example: 25
            type: integer
            minimum: 1
            maximum: 100
          description: Maximum number of items per page. Defaults to 25. Must be between 1 and 100 — a larger value is rejected with a 422 rather than clamped, so a page is never silently smaller than you asked for.
        - in: query
          name: include_deleted
          schema:
            description: When true, deleted worksheets are included, each carrying the `deleted_at` timestamp. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
            example: false
            type: boolean
          description: When true, deleted worksheets are included, each carrying the `deleted_at` timestamp. Defaults to false. Accepts `true`/`1`/`on` (case-insensitive) as true, and `false`/`0`/`off`/`""` or omission as false; any other value is rejected with a 422.
        - in: query
          name: include
          schema:
            description: Set to `pages` to get every listed worksheet's pages, with their text and image links, in the same response. One request instead of one per worksheet.
            example: pages
            type: string
            const: pages
          description: Set to `pages` to get every listed worksheet's pages, with their text and image links, in the same response. One request instead of one per worksheet.
      responses:
        "200":
          description: The practitioner's worksheets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetListResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    post:
      operationId: createPartnerWorksheet
      summary: Create a worksheet
      description: Creates a worksheet in the acting practitioner's library from page images you supply, and returns it in the same shape as a get-by-id. Send the pages base64-encoded in reading order; PNG, JPEG, GIF and WebP are accepted and the format is detected from the bytes. The whole page set travels in one request, so keep the encoded body under roughly 4MB. The new worksheet is immediately eligible to be placed on a playroom or toolkit shelf — its `shelf_ready` is true — and can be attached straight away. `description`, `category` and `tags` are optional and are returned on every subsequent read. Reuse the same Idempotency-Key on a retry to avoid creating a duplicate. The practice must have the creative suite enabled. REQUIRES a delegated token.
      tags:
        - Worksheets
      parameters:
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: The worksheet to create, with its page images.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerWorksheetCreateBody"
      responses:
        "201":
          description: The worksheet was created. The body is the same shape as a get-by-id, with its pages. `Location` points at the worksheet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}:
    get:
      operationId: getPartnerWorksheet
      summary: Get a worksheet by id
      description: "Returns one worksheet belonging to the practitioner the delegated token is acting as, with its pages in reading order. Page text and page images are clinician-authored content and the image links are short-lived, so treat the payload as clinical content: render it, do not log it. A worksheet with no pages returns an empty `pages` array. A deleted worksheet is a 404 unless `include_deleted=true`. REQUIRES a delegated token."
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The worksheet at the requested id, with its pages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetDetailResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
    delete:
      operationId: deletePartnerWorksheet
      summary: Delete a worksheet
      description: "Removes a worksheet from the acting practitioner's library. This is a soft delete: the worksheet drops out of the list, it is removed from any playroom or toolkit that included it, and its stored images are cleared. A repeated delete on the same id returns 404, not an error. A PlaySpace-supplied default worksheet cannot be deleted and returns 422. REQUIRES a delegated token."
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      responses:
        "204":
          description: The worksheet was deleted. No response body.
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/pages:
    get:
      operationId: listPartnerWorksheetPages
      summary: List a worksheet's pages
      description: "Returns every page of the worksheet in reading order, with its text and a link to its artwork. Not paginated — a worksheet is small. Useful on its own because the image links expire after an hour: re-read this to refresh them without pulling the whole worksheet. REQUIRES a delegated token."
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The pages of the worksheet, in reading order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetPagesResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/pages/{pageId}:
    get:
      operationId: getPartnerWorksheetPage
      summary: Get one page of a worksheet
      description: Returns a single page — its position in the worksheet, its text, and a link to its artwork. A page id that belongs to a different worksheet returns 404, the same as one that does not exist. REQUIRES a delegated token.
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet the page belongs to.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet the page belongs to.
        - in: path
          name: pageId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the page, as returned on the pages response as `data[].id`.
            example: 0d9c8b7a-6f5e-4d4c-8b3a-2f1e0d9c8b7a
          required: true
          description: ID of the page, as returned on the pages response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The requested page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetPageResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/pages/{pageId}/image:
    get:
      operationId: downloadPartnerWorksheetPageImage
      summary: Download a page's image
      description: "Returns the page image itself, as image bytes rather than a link. Use this when you need to store or re-serve the artwork: the `image_url` on the read endpoints is a temporary link that stops working after an hour, while this endpoint keeps working for as long as the worksheet exists. This is the stored page background; anything drawn on top of it is baked in by the PDF download, not by this endpoint. A page with no image returns 404. REQUIRES a delegated token."
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet the page belongs to.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet the page belongs to.
        - in: path
          name: pageId
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the page, as returned on the pages response as `data[].id`.
            example: 0d9c8b7a-6f5e-4d4c-8b3a-2f1e0d9c8b7a
          required: true
          description: ID of the page, as returned on the pages response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The page image. The content type is detected from the image itself.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/cover:
    get:
      operationId: downloadPartnerWorksheetCover
      summary: Download a worksheet's cover image
      description: Returns the cover image itself, as image bytes rather than a link — the counterpart to the page image endpoint, and the durable alternative to the temporary `thumbnail_url` on the read endpoints. A worksheet with no cover returns 404. REQUIRES a delegated token.
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The cover image. The content type is detected from the image itself.
          content:
            image/*:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/download:
    get:
      operationId: downloadPartnerWorksheet
      summary: Download a worksheet as a PDF
      description: Returns the worksheet as a PDF — one page per worksheet page, the same file the clinician downloads in PlaySpace, with anything drawn on a page composited onto its background. The response body is the PDF itself, not a JSON envelope, and the filename is carried on `Content-Disposition`. A worksheet with no pages returns 422 rather than an empty file. Rendering fetches and composites every page image, so this call is slower than the other reads. REQUIRES a delegated token.
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: query
          name: include_deleted
          schema:
            description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
            example: false
            type: boolean
          description: When true, a deleted worksheet is returned (with its `deleted_at`) instead of answering 404, and its pages, images, cover and PDF stay readable. Defaults to false.
      responses:
        "200":
          description: The worksheet as a PDF document.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
  /v1/partner/worksheets/{id}/share:
    post:
      operationId: sharePartnerWorksheet
      summary: Email a worksheet to a patient
      description: "Emails the patient a secure link to view the worksheet, valid for the requested window. The address is taken from the patient's own record — there is no recipient field, so a worksheet can only ever reach the patient you name, and that patient must be on the acting practitioner's current roster; a patient with no email on file is a 422. A worksheet with no pages is a 422 rather than a dead link, and only library worksheets can be sent — a copy a client has already worked on is not reachable here. Re-sending to the same patient extends the existing link instead of issuing a second one. Send a fresh Idempotency-Key per send: a repeated key returns the original result WITHOUT sending again. REQUIRES a delegated token."
      tags:
        - Worksheets
      parameters:
        - in: path
          name: id
          schema:
            type: string
            pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
            format: uuid
            description: ID of the worksheet, as returned on the list response as `data[].id`.
            example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
          required: true
          description: ID of the worksheet, as returned on the list response as `data[].id`.
        - in: header
          name: idempotency-key
          schema:
            type: string
            minLength: 1
            maxLength: 255
            description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
            example: 4f1c1aa7-9e3c-4f9b-9d4f-8e2c8b6a1c3d
          required: true
          description: Required on every POST, PATCH, and DELETE. An opaque, caller-chosen key that must be unique per logical operation. See the Idempotency section of the API introduction for the full retry contract.
      requestBody:
        required: true
        description: Which patient to send to, optionally how long the link should last, and an optional note for the email.
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PartnerContentShareBody"
      responses:
        "200":
          description: The email was accepted for delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartnerWorksheetShareResponse"
        "401":
          description: Unauthorized — the request is missing a valid bearer token.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "403":
          description: Forbidden — the token is valid but does not authorize this action.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "404":
          description: Not Found — no resource exists at this id.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "409":
          description: Idempotency Conflict — the Idempotency-Key was reused while the original request is still in flight.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "422":
          description: Validation Error — the request body, query, or path parameters did not match the schema.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "429":
          description: Rate Limited — the organization has exceeded its rate-limit budget for this window. Retry after the window resets.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
        "500":
          description: Internal Error — an unexpected condition was encountered. Quote the X-Request-Id header when contacting support.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Problem"
components:
  schemas:
    PartnerAppointmentCreateBody:
      type: object
      properties:
        video:
          $ref: "#/components/schemas/PartnerAppointmentVideoInput"
        partner_clinic_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: Identifier of the clinic this appointment is booked under. The practitioner and patient must both belong to this clinic. An unknown or inaccessible id is rejected with a 422.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: Identifier of the practitioner the appointment is scheduled with. Must belong to `partner_clinic_id`; an unknown or inaccessible id is rejected with a 422.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: Identifier of the patient the appointment is for. Must belong to `partner_clinic_id`; an unknown or inaccessible id is rejected with a 422.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
          description: Appointment start as an ISO-8601 offset-aware datetime. Must be strictly before `end_at`. There is no future-only constraint, so historical appointments may be backfilled.
          example: 2026-06-02T15:00:00.000Z
        end_at:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
          description: Appointment end as an ISO-8601 offset-aware datetime. Must be strictly after `start_at` (otherwise a 422).
          example: 2026-06-02T15:50:00.000Z
        session_type:
          type: string
          enum:
            - virtual
            - in_person
          description: Session delivery mode. `virtual` provisions a video session and returns its join link on `patient_video_url`; `in_person` does not.
          example: virtual
        timezone:
          description: Optional IANA timezone (e.g. `America/New_York`) for rendering `start_at` / `end_at` in local wall-clock time. Validated against the IANA timezone database when present, so an unknown zone is a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. When omitted or `null`, the appointment inherits the booking clinic timezone.
          example: America/New_York
          anyOf:
            - type: string
            - type: "null"
        notes:
          description: Optional free-text appointment notes (up to 5000 characters). May contain protected health information. Omit or send `null` when none.
          example: Initial intake session.
          anyOf:
            - type: string
              maxLength: 5000
            - type: "null"
        notify_patient:
          default: false
          description: When `true`, the patient receives a booking-confirmation email. Defaults to `false`. This flag is not stored on the appointment; the clinician confirmation and calendar updates happen regardless.
          example: false
          type: boolean
        notify_practitioner:
          default: false
          description: When `true`, the clinician receives a booking-confirmation email carrying their own (host) join link. Defaults to `false`. This flag is not stored on the appointment.
          example: false
          type: boolean
      required:
        - partner_clinic_id
        - partner_practitioner_id
        - partner_patient_id
        - start_at
        - end_at
        - session_type
      additionalProperties: false
      title: PartnerAppointmentCreateBody
      description: Body of `POST /v1/partner/appointments`. Books a real appointment that the clinician sees immediately. The practitioner and patient must both belong to `partner_clinic_id`, otherwise a 422 (`same-clinic-mismatch`). Conflicts are checked only against appointments your organization has already booked for this practitioner and return a 409; appointments booked elsewhere are not considered. Unknown keys are rejected. `notes` may contain protected health information, and `notify_patient` is a non-persisted behavior flag.
      example:
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at: 2026-06-02T15:00:00.000Z
        end_at: 2026-06-02T15:50:00.000Z
        session_type: virtual
        timezone: America/New_York
        notes: Initial intake session.
        notify_patient: false
    PartnerAppointmentVideoInput:
      type: object
      properties:
        clinician_url:
          type: string
          minLength: 1
          maxLength: 8192
        patient_url:
          type: string
          minLength: 1
          maxLength: 8192
        label:
          type: string
          minLength: 1
          maxLength: 80
        expires_at:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
        presentation:
          default: inline
          type: string
          enum:
            - inline
            - external
      required:
        - clinician_url
        - patient_url
      additionalProperties: false
      description: Complete clinician and patient HTTPS video URLs from an approved origin. Required for every new virtual appointment after PlaySpace enables supplied video. Resubmitting replaces both URLs for the next join or explicit reload; ongoing calls do not switch. Optional expiry must cover the appointment end. PlaySpace audio recording is independent of provider video and remains subject to organization recording settings and clinician access.
    PartnerAppointmentUpdateBody:
      type: object
      properties:
        video:
          description: Complete replacement of both supplied video URLs. Required when changing the times of an appointment that already uses supplied video; omission preserves video on other edits. Video ownership cannot be changed or removed.
          $ref: "#/components/schemas/PartnerAppointmentVideoInput"
        start_at:
          format: date-time
          description: "New appointment start as an ISO-8601 offset-aware datetime. Coupled with `end_at`: supply both together to reschedule, and supplying exactly one is a 422. When supplied, must be strictly before `end_at`. Omit both to leave the window unchanged."
          example: 2026-06-02T16:00:00.000Z
          type: string
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
        end_at:
          format: date-time
          description: "New appointment end as an ISO-8601 offset-aware datetime. Coupled with `start_at`: supply both together, and supplying exactly one is a 422. Must be strictly after `start_at`. Omit both to leave the window unchanged."
          example: 2026-06-02T16:50:00.000Z
          type: string
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
        timezone:
          description: IANA timezone (e.g. `America/New_York`) for rendering `start_at` / `end_at` in local wall-clock time. Validated against the IANA timezone database when present, so an unknown zone is a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. Send `null` to clear it (the appointment then renders against the booking clinic timezone); omit to leave it unchanged.
          example: America/New_York
          anyOf:
            - type: string
            - type: "null"
        notes:
          description: Free-text appointment notes (up to 5000 characters). May contain protected health information. Send `null` to clear it; omit to leave it unchanged.
          example: Rescheduled at patient request.
          anyOf:
            - type: string
              maxLength: 5000
            - type: "null"
        status:
          description: "New appointment status, one of `scheduled`, `completed`, `cancelled`, or `no_show`. Setting `cancelled` is the full cancellation path: it removes the appointment from the practitioner's connected calendars and, when `notify_patient` is `true`, emails the patient a cancellation. A cancelled appointment then drops from the default list (surface it with `include_deleted=true`), and any later `GET` or `PATCH` of that id returns a 404. Omit to leave the status unchanged."
          example: completed
          type: string
          enum:
            - scheduled
            - completed
            - cancelled
            - no_show
        notify_patient:
          default: false
          description: When `true`, the patient receives a reschedule or status-change notification email. Defaults to `false`. This flag is not stored on the appointment and is not a substantive change on its own, so a body carrying only `notify_patient` is rejected with a 422.
          example: false
          type: boolean
        notify_practitioner:
          default: false
          description: When `true`, the clinician receives a reschedule, status-change, or cancellation notification email (a reschedule carries their own join link). Defaults to `false`. Not stored, and not a substantive change on its own, so a body carrying only `notify_practitioner` is rejected with a 422.
          example: false
          type: boolean
      additionalProperties: false
      title: PartnerAppointmentUpdateBody
      description: Partial-update body for `PATCH /v1/partner/appointments/{id}` (reschedule, change status, edit notes, or cancel). All fields are optional, but at least one substantive field is required — the coupled `start_at` / `end_at` pair, `timezone`, `notes`, or `status` (`notify_patient` alone is not substantive). `start_at` and `end_at` are coupled (both or neither; sending one is a 422), and when both present `end_at` must be strictly after `start_at`. Setting `status` to `cancelled` is the full cancellation path, which drops the appointment from the default list and returns a 404 on a later `GET` or `PATCH`. `session_type` and the scoping ids cannot be patched, and unknown keys are rejected. `notes` may contain protected health information, and `notify_patient` is a non-persisted behavior flag.
      example:
        start_at: 2026-06-02T16:00:00.000Z
        end_at: 2026-06-02T16:50:00.000Z
        notes: Rescheduled at patient request.
        notify_patient: false
    ClinicCommunityItemType:
      type: string
      enum:
        - worksheet
        - storybook
        - form
      description: Which kind of content a clinic-community item is. Games are not shared through this API.
      example: worksheet
    ClinicCommunityPublishBody:
      type: object
      properties:
        item_type:
          $ref: "#/components/schemas/ClinicCommunityItemType"
        item_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: ID of the worksheet, storybook or form in the acting practitioner's own library, as returned by that resource's list endpoint.
          example: a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d
        title:
          type: string
          minLength: 1
          maxLength: 200
          description: Title colleagues will see. Defaults are not applied; send the title you want shown.
          example: Feelings thermometer
        description:
          description: Optional note for colleagues about when to use this.
          example: Scaling exercise for the first ten minutes of a session.
          type: string
          minLength: 1
          maxLength: 2000
        age_categories:
          default: []
          description: Who it suits. Any of `child`, `teen`, `adult`; empty when unspecified.
          example:
            - child
          maxItems: 3
          type: array
          items:
            type: string
            enum:
              - child
              - teen
              - adult
        tags:
          default: []
          description: Free-text tags, up to 20 of up to 50 characters each.
          example:
            - anxiety
            - scaling
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
      required:
        - item_type
        - item_id
        - title
      additionalProperties: false
      title: ClinicCommunityPublishBody
      description: Which of your content to share with the clinic, and how to present it. Unknown fields are rejected.
    PartnerClinicCreateBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Clinic display name. Trimmed and non-empty, max 200 characters. Names are unique per partner and compared case-insensitively; a name already in use by another of your clinics returns 409 `clinic-name-conflict`.
          example: Downtown Therapy Center
        timezone:
          type: string
          description: IANA timezone identifier for the clinic (e.g. `America/New_York`). Validated against the IANA timezone database; an unknown or malformed zone (e.g. `America/New_Yrok`) is rejected with a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. Applied to every practitioner created under this clinic.
          example: America/New_York
      required:
        - name
        - timezone
      additionalProperties: false
      title: PartnerClinicCreateBody
      description: "Body of `POST /v1/partner/clinics`: the clinic name and IANA timezone. Unknown keys are rejected."
      example:
        name: Downtown Therapy Center
        timezone: America/New_York
    PartnerClinicUpdateBody:
      type: object
      properties:
        name:
          description: Clinic display name. When supplied, trimmed and non-empty, max 200 characters; omit to leave unchanged. Setting it to the clinic's current name is a 200 no-op. Renaming to a name already in use by another of your clinics returns 409 `clinic-name-conflict` (names are unique per partner, compared case-insensitively).
          example: Uptown Therapy Center
          type: string
          minLength: 1
          maxLength: 200
        timezone:
          description: IANA timezone identifier for the clinic (e.g. `America/Chicago`). When supplied, validated against the IANA timezone database; an unknown or malformed zone (e.g. `America/New_Yrok`) is rejected with a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. Omit to leave unchanged. Setting it to the clinic's current timezone is a 200 no-op.
          example: America/Chicago
          type: string
      additionalProperties: false
      title: PartnerClinicUpdateBody
      description: Partial-update body for `PATCH /v1/partner/clinics/{id}`. At least one of `name`, `timezone` is required. Unknown keys are rejected. Setting a field to its current value is a 200 no-op.
      example:
        name: Uptown Therapy Center
        timezone: America/Chicago
    PartnerEmbedTokenCreateBody:
      type: object
      properties:
        capabilities:
          minItems: 1
          type: array
          items:
            $ref: "#/components/schemas/EmbedCapability"
          description: Actions to grant the embedded surface. Request the narrowest set the screen needs — a read-only viewer should not carry storybook:create.
          example:
            - storybook:read
            - storybook:create
        origins:
          minItems: 1
          type: array
          items:
            type: string
            format: uri
          description: Absolute origins permitted to frame the embed, e.g. https://app.yourclinic.com. Becomes the frame-ancestors directive on the embed document; a host whose origin is not listed cannot render the iframe at all. A wildcard is accepted only as a whole leading label (https://*.yourclinic.com); a wildcard inside a label (https://app-*.yourclinic.com) is not a valid CSP source expression and is rejected here, because a browser would silently discard it and refuse to frame the embed from anywhere.
          example:
            - https://app.yourclinic.com
        patient_id:
          format: uuid
          description: "Patient the embedded surface acts on, as the PlaySpace patient id returned by /v1/partner/patients. REQUIRED when capabilities include form:submit, form:send, storybook:share or worksheet:share: a form submission is patient data and must have a subject, and a send reads that patient's address and records a pending submission against them. The patient must belong to your organization AND be on the delegated practitioner's roster; a patient outside either is rejected with 403, not silently ignored. Omit it for surfaces that only read."
          example: 3f1b0e2a-9c4d-4f4b-8a1e-2d6c5b7a9e01
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
        ttl_seconds:
          description: Lifetime of the returned token in seconds. Defaults to 900 (15 minutes) and is capped at 3600. Mint one per page load rather than reusing a long-lived token.
          example: 900
          type: integer
          minimum: 60
          maximum: 3600
      required:
        - capabilities
        - origins
      title: PartnerEmbedTokenCreateBody
      description: "Request body for POST /v1/partner/embed-tokens. The practitioner is NOT specified here — it is taken from the delegated token, so a caller cannot mint a token acting as someone it was not delegated to. The patient, by contrast, IS named here: one clinician sees many patients, so there is nothing in the delegated token to infer it from."
    EmbedCapability:
      type: string
      enum:
        - storybook:read
        - storybook:create
        - storybook:write
        - storybook:delete
        - form:read
        - form:create
        - form:submit
        - form:write
        - form:delete
        - form:compose
        - form:send
        - worksheet:read
        - worksheet:create
        - worksheet:write
        - worksheet:delete
        - worksheet:generate
        - games:play
        - note:read
        - note:write
        - client:read
        - appointment:read
        - appointment:write
        - shell:read
        - playroom:read
        - playroom:write
        - session:launch
        - data:export
        - storybook:share
        - worksheet:share
        - community:read
        - community:write
      description: "A single action the embedded surface may perform. Capabilities are checked per request by the embed API tier; a token without storybook:create cannot start a generation, one without form:create cannot author a new form template, one without form:submit can render a form but not save an answer, one without worksheet:read cannot open a worksheet, one without worksheet:create cannot upload a PDF, start a blank worksheet or duplicate one, one without worksheet:write cannot edit an existing worksheet, one without worksheet:delete cannot remove one, and one without worksheet:generate cannot ask the image generator for artwork to place on a page. The four worksheet verbs are deliberately separate: worksheet:create ADDS a new worksheet (from a PDF, blank, or as a copy of one the clinician already has), worksheet:write CHANGES one that already exists — its pages, its publish state and where it sits on a shelf, including adding, reordering and removing pages — worksheet:delete REMOVES one from the library, and worksheet:generate is the only one that SPENDS MONEY, so a host can mint a drawing seat that cannot bill. storybook:write is the storybook sibling of that middle verb: it publishes and shelves a storybook that already exists, rewrites a page’s text, accepts a regenerated illustration and reorders the pages, and it neither starts a generation (storybook:create, which also covers the two paid edits — regenerating a page’s illustration and adding a page) nor opens one (storybook:read). storybook:delete removes a storybook from the library, a soft delete that also takes it off every room shelf it sat on; it is its own verb for the reason worksheet:delete is. Request only the verbs the screen actually needs. form:read covers a form template AND the responses recorded against it, because a submission is meaningless without the template it answers, so a token that could read one and not the other buys a surface that cannot render. form:write CHANGES a form that already exists — its name, its description and its questions — and form:delete RETIRES one from the library, a soft delete that also takes it off every room shelf it sat on and leaves every recorded response intact and readable. The three form authoring verbs are separate for the reason the worksheet verbs are: form:create ADDS a new template, form:write REWRITES an instrument a clinician’s patients may already have answered, and form:delete REMOVES one, so a host can mint a builder that cannot rewrite history and an editor that cannot destroy a library. Neither carries the authoring state: a form reaches published at creation or through the clinician’s own application, and no partner-facing tier can promote a draft. Neither publishes or shelves either. form:compose lets the clinician place a form on one of their own playroom or toolkit shelves from inside the frame, which is what makes it reachable during a live session; it can only place a form that is already shelf-ready, and it never creates or deletes a room. note:read opens the acting clinician’s own clinical notes read-only, list and detail, and it needs no patient_id because a note is the clinician’s record and the list spans every patient they have seen. note:write is the separately licensable write half of that same area, reachable only from inside the framed workspace: the clinician may edit the active version of one of their own notes, sign it — which locks a new version in place — and unlock one they hold. It is never implied by note:read, because a host that bought the clinical-notes area to read has not thereby bought one that can sign or unlock a clinical record, and the server-to-server note endpoints stay read-only whatever is minted. It needs no patient_id for the same reason note:read does not. shell:read frames the WHOLE PlaySpace workspace rather than one artifact surface: the clinician’s real navigation, header and feature gating. It grants no data of its own — every area inside the workspace is still gated by its own capability, so mint shell:read alongside the areas the seat should reach, and a shell:read token with no note:read frames the product with the notes area refused. It is also refused outright on the patient seat of a game session. client:read and appointment:read open the acting clinician’s own client roster and appointment book, read-only, as areas inside that workspace; neither needs a patient_id, because both are lists spanning the clinician’s whole caseload rather than a record about one patient, and the appointment projection deliberately carries no meeting join link. client:read also covers a single client’s usage summary — counts and labels of what that client has been given and has returned, never the bodies. appointment:write books, reschedules and cancels an appointment in that clinician’s own calendar from inside the framed workspace; it is separate from appointment:read on purpose, because a host that bought a calendar to read has not thereby bought one that writes into a clinician’s book. An appointment booked through the frame always uses your organization’s platform video provider — a framed document holds no meeting URLs of yours, so the write accepts none and is never asked for any, whatever your supplied-video configuration says. It needs no patient_id: the row is a calendar entry on a caseload-wide seat, and the patient is named per request by partner-namespace id. form:send asks PlaySpace to email the bound patient a link to fill one of the clinician’s forms in: PlaySpace reads the address it holds for that patient, records the pending submission, mints the fill link and hands it to its email provider, so the link is never returned to you and no long-lived PlaySpace credential ever reaches your application. playroom:read lists the acting clinician’s own playrooms, opens one, and shows what is on its shelf; playroom:write creates a playroom and changes one — its name, description, room type, colour palette, tags, enabled session items and published state. Neither needs a patient_id, because a playroom is a room rather than a record about one patient. This tier has NO room delete and no attach or detach verb of its own: playroom:write cannot remove or restore a room, and taking an item off a shelf stays with worksheet:write, storybook:write and form:compose — but creating a room does place content on it, because the create back-fills the new room from the clinician’s own shelf-ready library. session:launch lets the clinician open a live session for one of their own appointments INSIDE the framed workspace, in place of your application opening a tab: the frame mints that clinician’s own join credential and renders the session in its content pane. It is separate from appointment:read on purpose — the calendar projection deliberately carries no meeting join link — and it needs no patient_id, because the subject is an appointment on a caseload-wide seat. It returns nothing to your application: the patient’s join link is never handed to a framed document, so mint that yourself from POST /v1/partner/appointments/{id}/session-links if you have a patient-facing surface of your own. data:export lets the clinician start a copy of their WHOLE PlaySpace record from inside the framed workspace — every client, with their notes, forms, worksheets, storybooks and uploaded files — watch it build, and download it as one ZIP file; it is the same archive POST /v1/partner/exports produces. Grant it deliberately: it is not implied by client:read or shell:read, and it needs no patient_id because the export spans the whole caseload. form:submit and form:send both additionally require patient_id — each is about one named patient, so the token must name them. storybook:share and worksheet:share show a Send to client button inside the reader or viewer that emails the named patient a secure link to that storybook or worksheet; both require patient_id for the same reason, and the frame never chooses a recipient. community:read shows the clinic community inside the framed workspace, the worksheets, storybooks and forms colleagues in the same clinic have shared, and lets the clinician preview one; community:write adds Share with clinic on the framed worksheet, storybook and form screens, Copy to my library and Remove in that area. Neither needs patient_id."
      example: storybook:create
    PartnerFormCreateBody:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
          description: Clinician-facing form name.
          example: Intake questionnaire
        description:
          description: Optional longer description.
          anyOf:
            - type: string
              maxLength: 2000
            - type: "null"
        fields:
          default: []
          description: The form definition, in the same shape PlaySpace's own builder produces. Every field needs a unique `id` and a `type` the embedded surface can render — a duplicate id or an unsupported type is refused with a 422 naming the offenders, because a form saved with either would fail to open.
          type: array
          items: {}
        status:
          default: draft
          description: Authoring lifecycle. Defaults to `draft`, which is deliberately invisible everywhere else on this API — a draft does not appear in the list, cannot be read by id, and cannot be attached to a shelf. Send `published` unless you intend to finish the form later in PlaySpace.
          example: published
          type: string
          enum:
            - draft
            - published
      required:
        - name
      additionalProperties: false
      title: PartnerFormCreateBody
      description: A new form owned by the practitioner the token is acting as.
    PartnerFormUpdateBody:
      type: object
      properties:
        shelf_ready:
          type: boolean
          const: true
          description: "Set to `true` to make this form eligible for a playroom or toolkit shelf, which is what an attach requires. Only `true` is accepted: withdrawing a form also removes it from every shelf it is on and cannot restore what it was before, so it is not offered as the opposite of this flag."
          example: true
      required:
        - shelf_ready
      additionalProperties: false
      title: PartnerFormUpdateBody
      description: What to change about a form. Today the only change on offer is making it shelf-eligible.
    PartnerGameType:
      type: string
      enum:
        - sandtray
        - dollhouse
      description: "Which scene the session runs: a sandtray or a dollhouse. Fixed for the life of the session."
      example: sandtray
    PartnerGameSessionCreateBody:
      type: object
      properties:
        game_type:
          $ref: "#/components/schemas/PartnerGameType"
        patient_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: The patient who will play, as one of your own patient ids. Scenes saved during the session are filed against this patient. The patient must be on the acting clinician's roster.
          example: 7c9e6679-7425-40de-944b-e07fc1f90ae7
        origins:
          minItems: 1
          maxItems: 8
          type: array
          items:
            type: string
            format: uri
          description: Absolute origins permitted to frame either seat, e.g. https://app.yourclinic.com. Becomes the frame-ancestors directive on both embed documents; a host whose origin is not listed cannot render the iframe at all. A wildcard is accepted only as a whole leading label (https://*.yourclinic.com); a wildcard inside a label is not a valid source expression and is rejected here, because a browser would silently discard it and refuse to frame the embed from anywhere.
          example:
            - https://app.yourclinic.com
        game_session_id:
          format: uuid
          description: Omit to start a new session. Pass the `game_session_id` from an earlier response to re-issue fresh seat tokens for a session already in progress — required when refreshing tokens mid-session, because a frame refuses a token minted for a different session.
          example: a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
        token_ttl_seconds:
          description: "Lifetime of both returned tokens in seconds. Defaults to 900 (15 minutes) and is capped at 3600. A session outliving its tokens keeps working: re-mint and push a fresh token rather than reloading the frame."
          example: 900
          type: integer
          minimum: 300
          maximum: 3600
      required:
        - game_type
        - patient_id
        - origins
      additionalProperties: false
      title: PartnerGameSessionCreateBody
      description: Which scene to run, for which patient, and which of your origins may frame it. The clinician is NOT specified here — it is taken from the delegated token, so a caller cannot start a session acting as someone it was not delegated to.
    PartnerPatientCreateBody:
      type: object
      properties:
        partner_clinic_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: ID of the clinic this patient is created under; the patient is anchored to this clinic. Must reference one of your active clinics.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        created_by_partner_practitioner_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: ID of the practitioner credited as the creator of this patient. The patient is linked to this practitioner on creation. Must reference one of your active practitioners, in any clinic (not restricted to `partner_clinic_id`). Echoed back on the response.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        first_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Patient given name. Trimmed; required non-empty; max 100 characters.
          example: Avery
        last_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Patient family name. Trimmed; required non-empty; max 100 characters.
          example: Lee
        preferred_name:
          description: Optional patient preferred name. Trimmed when present; max 100 characters. Omit or send null when not recorded.
          example: Alex
          anyOf:
            - type: string
              maxLength: 100
            - type: "null"
        date_of_birth:
          format: date
          description: Optional patient date of birth as an ISO-8601 calendar date (YYYY-MM-DD, no time component). A malformed or impossible date (e.g. 2026-02-30) or a value carrying a time component is a 422. Omit or send null when not recorded.
          example: 1998-03-14
          anyOf:
            - type: string
              pattern: ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
            - type: "null"
        email:
          description: "Optional patient email. Trimmed and lower-cased; the canonical form is echoed back on the response. When supplied, must be a syntactically valid email address — a malformed value is a 422. No uniqueness check: a duplicate email is accepted (see the Patients tag). Omit or send null when not recorded."
          example: avery.lee@example.com
          anyOf:
            - type: string
              maxLength: 254
              format: email
              pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
            - type: "null"
        phone_number:
          description: Optional patient phone number. Stored verbatim with no normalization; max 50 characters. The empty string is allowed and means "no phone recorded". Omit or send null when not recorded.
          example: +1-555-0123
          anyOf:
            - type: string
              maxLength: 50
            - type: "null"
        timezone:
          description: Optional patient IANA timezone (e.g. America/New_York). When present, must be a valid IANA zone; an unknown or malformed zone is a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. Omit or send null when not recorded.
          example: America/New_York
          anyOf:
            - type: string
            - type: "null"
      required:
        - partner_clinic_id
        - created_by_partner_practitioner_id
        - first_name
        - last_name
      additionalProperties: false
      title: PartnerPatientCreateBody
      description: Body of POST /v1/partner/patients. `first_name`, `last_name`, `partner_clinic_id`, and `created_by_partner_practitioner_id` are required; `preferred_name`, `date_of_birth`, `email`, `phone_number`, and `timezone` are optional and nullable. Unknown keys are rejected with a 422. Email has no uniqueness check (see the Patients tag).
      example:
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        created_by_partner_practitioner_id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        first_name: Avery
        last_name: Lee
        preferred_name: Alex
        date_of_birth: 1998-03-14
        email: avery.lee@example.com
        phone_number: +1-555-0123
        timezone: America/New_York
    PartnerPatientUpdateBody:
      type: object
      properties:
        first_name:
          description: Patient given name. When supplied, trimmed and required non-empty; max 100 characters. Omit to leave unchanged.
          example: Avery
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          description: Patient family name. When supplied, trimmed and required non-empty; max 100 characters. Omit to leave unchanged.
          example: Lee
          type: string
          minLength: 1
          maxLength: 100
        preferred_name:
          description: Patient preferred name. Trimmed when present; max 100 characters. Send null to clear it, omit to leave unchanged.
          example: Alex
          anyOf:
            - type: string
              maxLength: 100
            - type: "null"
        date_of_birth:
          format: date
          description: Patient date of birth as an ISO-8601 calendar date (YYYY-MM-DD, no time component). A malformed or impossible date (e.g. 2026-02-30) or a value carrying a time component is a 422. Send null to clear it, omit to leave unchanged.
          example: 1998-03-14
          anyOf:
            - type: string
              pattern: ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
            - type: "null"
        email:
          description: "Patient email. Trimmed and lower-cased; the canonical form is echoed back on the response. When supplied, must be a syntactically valid email address — a malformed value is a 422. No uniqueness check: setting any value, including another patient's email, succeeds (see the Patients tag). Send null to clear it, omit to leave unchanged."
          example: avery.lee@example.com
          anyOf:
            - type: string
              maxLength: 254
              format: email
              pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
            - type: "null"
        phone_number:
          description: Patient phone number. Stored verbatim with no normalization; max 50 characters. The empty string is allowed and means "no phone recorded". Send null to clear it, omit to leave unchanged.
          example: +1-555-0123
          anyOf:
            - type: string
              maxLength: 50
            - type: "null"
        timezone:
          description: Patient IANA timezone (e.g. America/New_York). When present, must be a valid IANA zone; an unknown or malformed zone is a 422. Aliases such as `Asia/Calcutta` and the bare `UTC` are accepted; a numeric UTC offset such as `+05:30` is not a named zone and is rejected. Send null to clear it, omit to leave unchanged.
          example: America/New_York
          anyOf:
            - type: string
            - type: "null"
      additionalProperties: false
      title: PartnerPatientUpdateBody
      description: Partial-update body for PATCH /v1/partner/patients/{id}. At least one of `first_name`, `last_name`, `preferred_name`, `date_of_birth`, `email`, `phone_number`, or `timezone` is required. Unknown keys are rejected with a 422. `partner_clinic_id` and `created_by_partner_practitioner_id` are not accepted — both are fixed at creation. Email has no uniqueness check (see the Patients tag).
      example:
        preferred_name: Alex
        phone_number: +1-555-0199
    PartnerPlayroomCreateBody:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          description: Room name shown in the picker. Trimmed and non-empty, max 200 characters.
          example: Calm Down Corner
        description:
          description: Optional note about when to use the room. Max 1000 characters.
          example: For the first ten minutes of a session with younger clients.
          type: string
          maxLength: 1000
        room_type:
          default: adult
          description: Age band the room art is pitched at. Defaults to `adult`.
          $ref: "#/components/schemas/PartnerPlayroomRoomType"
        color_palette:
          default: color1
          description: Colour theme for the room. Defaults to `color1`.
          $ref: "#/components/schemas/PartnerPlayroomColorPalette"
        session_items:
          description: Items to enable inside the room. Omit to get the standard set (everything except wall posters). An empty array creates a room with no activities.
          example:
            - activity_shelf
            - whiteboard
            - sand_tray
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
        tags:
          description: Optional labels, at most 20, each 1-50 characters. Defaults to none.
          example:
            - anxiety
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        published:
          default: true
          description: Whether the room is immediately offered at session start. Defaults to true; send false to create it as a draft.
          example: true
          type: boolean
      required:
        - title
      additionalProperties: false
      title: PartnerPlayroomCreateBody
      description: Body of `POST /v1/partner/playrooms`. Only `title` is required; the rest fall back to the same defaults the PlaySpace app applies. Unknown keys are rejected.
      example:
        title: Calm Down Corner
        room_type: child
        session_items:
          - activity_shelf
          - whiteboard
          - sand_tray
        published: true
    PartnerPlayroomRoomType:
      type: string
      enum:
        - adult
        - child
        - teen
      title: PartnerPlayroomRoomType
      description: Age band the room art and copy are pitched at. Drives which themed artwork the room renders.
      example: child
    PartnerPlayroomColorPalette:
      type: string
      enum:
        - color1
        - color2
        - color3
      title: PartnerPlayroomColorPalette
      description: Which of the three colour themes the room is decorated with. Combined with `room_type` it selects the room artwork and thumbnail.
      example: color1
    PartnerSessionItem:
      type: string
      enum:
        - activity_shelf
        - multiplayer_games
        - single_player_games
        - whiteboard
        - sand_tray
        - dollhouse
      title: PartnerSessionItem
      description: One interactive item that can be enabled inside a session space. `activity_shelf` holds worksheets, storybooks, and forms; `multiplayer_games` and `single_player_games` are the game libraries; `whiteboard`, `sand_tray`, and `dollhouse` are the standalone activities.
      example: sand_tray
    PartnerPlayroomUpdateBody:
      type: object
      properties:
        title:
          description: New room name. Trimmed and non-empty, max 200 characters.
          example: Calm Down Corner
          type: string
          minLength: 1
          maxLength: 200
        description:
          description: New note, or null to clear it. Max 1000 characters.
          example: null
          anyOf:
            - type: string
              maxLength: 1000
            - type: "null"
        room_type:
          description: New age band. Changing it re-derives `thumbnail_url`.
          example: teen
          $ref: "#/components/schemas/PartnerPlayroomRoomType"
        color_palette:
          description: New colour theme. Changing it re-derives `thumbnail_url`.
          example: color2
          $ref: "#/components/schemas/PartnerPlayroomColorPalette"
        session_items:
          description: Replacement set of enabled items — the array replaces the current one entirely.
          example:
            - activity_shelf
            - sand_tray
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
        tags:
          description: Replacement labels — the array replaces the current ones entirely.
          example:
            - anxiety
            - intake
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        published:
          description: Publish (`true`) or unpublish (`false`) the room.
          example: false
          type: boolean
      additionalProperties: false
      title: PartnerPlayroomUpdateBody
      description: Body of `PATCH /v1/partner/playrooms/{id}`. Any subset of the create fields; at least one is required. Array fields replace rather than merge. Unknown keys are rejected.
      example:
        title: Calm Down Corner
        published: false
    PartnerContentType:
      type: string
      enum:
        - worksheet
        - storybook
        - form
      title: PartnerContentType
      description: "Kind of content a shelf entry holds. `worksheet`, `storybook` and `form` are the three members defined today. **This is an open vocabulary on the way out.** PlaySpace adds content types over time — games are the next one — and a new member will begin appearing as `content_type` on these responses without a new API version, a new path, or any other advance signal. **Your client MUST ignore any `content_type` value it does not recognise**: skip the item, or render it generically. A client that throws, discards the whole response, or fails its own schema validation on an unknown member will break the first time a clinician puts a new kind of content on a shelf. On the way in the vocabulary is closed: a `contentType` path segment outside the list above is rejected with `422`."
      example: worksheet
    PartnerPractitionerCreateBody:
      type: object
      properties:
        first_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Practitioner given name.
          example: Jane
        last_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Practitioner family name.
          example: Doe
        email:
          type: string
          maxLength: 254
          format: email
          pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
          description: Practitioner work email, which becomes their login identity. Normalized to lowercase, and the canonical form is returned in the response. An email already in use by another practitioner returns a 409.
          example: jane.doe@clinic.example
        partner_clinic_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: Identifier of the clinic this practitioner belongs to. Must reference an active clinic you can access; otherwise the request fails validation with a 422 naming `partner_clinic_id`.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        role:
          description: "Optional membership role in the clinic: `member`, `admin`, or `owner`. Defaults to `member`. Informational metadata in this version; it does not affect API authorization."
          example: member
          type: string
          enum:
            - member
            - admin
            - owner
        country:
          description: Optional country this practitioner works in, as an uppercase ISO 3166-1 alpha-2 code such as `US` or `AU`. Lower-case spellings and three-letter codes are rejected. Omit to leave it unset; a practitioner with no country is treated as outside every country-restricted feature.
          example: US
          type: string
          pattern: ^[A-Z]{2}$
      required:
        - first_name
        - last_name
        - email
        - partner_clinic_id
      additionalProperties: false
      title: PartnerPractitionerCreateBody
      description: Fields for creating a practitioner.
      example:
        first_name: Jane
        last_name: Doe
        email: jane.doe@clinic.example
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        role: member
        country: US
    PartnerPractitionerUpdateBody:
      type: object
      properties:
        first_name:
          description: Practitioner given name. Omit to leave unchanged.
          example: Sam
          type: string
          minLength: 1
          maxLength: 100
        last_name:
          description: Practitioner family name. Omit to leave unchanged.
          example: Taylor
          type: string
          minLength: 1
          maxLength: 100
        email:
          description: Practitioner work email, which becomes their login identity. Normalized to lowercase, and the canonical form is returned in the response. Setting it to another practitioner's email returns a 409; setting it to the practitioner's own current email is a no-op. Omit to leave unchanged.
          example: sam.taylor@example.com
          type: string
          maxLength: 254
          format: email
          pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
        role:
          description: "Membership role in the clinic: `member`, `admin`, or `owner`. Informational metadata in this version; it does not affect API authorization. Omit to leave unchanged."
          example: admin
          type: string
          enum:
            - member
            - admin
            - owner
        status:
          description: Set to `disabled` to suspend the practitioner's per-clinician (delegated) access, or `active` to restore it. Disabling is reversible and keeps all records — the practitioner can be re-enabled at any time, and PlaySpace support can also restore access. Omit to leave unchanged.
          example: disabled
          type: string
          enum:
            - active
            - disabled
        country:
          description: The country this practitioner works in, as an uppercase ISO 3166-1 alpha-2 code such as `US` or `AU`. PlaySpace uses it to decide which region-restricted features this practitioner is offered, so setting it accurately changes what they can do. Lower-case spellings and three-letter codes are rejected. Send `null` to clear it, after which the practitioner is treated as outside every region-restricted feature. Omit to leave unchanged.
          example: AU
          anyOf:
            - type: string
              pattern: ^[A-Z]{2}$
            - type: "null"
      additionalProperties: false
      title: PartnerPractitionerUpdateBody
      description: Fields for updating a practitioner. Supply at least one of `first_name`, `last_name`, `email`, `role`, `status`, or `country`. A practitioner cannot be moved to a different clinic.
      example:
        first_name: Sam
        email: sam.taylor@example.com
        role: admin
        country: AU
    PartnerSessionConfigPatch:
      type: object
      properties:
        enabled_features:
          type: object
          propertyNames:
            type: string
          additionalProperties:
            type: boolean
          description: "Feature switches, keyed by feature name. Session keys — games, whiteboard, worksheets, storybook — control the in-session toolbar. The remaining keys control what your framed PlaySpace workspace offers, and can only take a feature away: setting one true never grants a feature your organization is not entitled to. A key you omit keeps whatever it had; an unrecognised key is rejected rather than ignored."
          example:
            clinical_notes: false
            storybook: false
        allowed_content:
          anyOf:
            - type: object
              properties:
                toolkit_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                playroom_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                worksheet_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                game_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
              description: Optional narrowing of the default session catalog to the listed items. Every id must be a UUID; a malformed id is refused rather than dropped, because a partially applied allowlist would curate the catalog in a way you did not ask for. Omit a key to leave that part of the catalog whole.
              example:
                game_ids: []
            - type: "null"
        video_provider:
          type: string
          enum:
            - Whereby
            - Dailyco
            - EightxEight
            - None
          description: "Which video vendor PlaySpace opens sessions with. 'None' means your organization runs video elsewhere: PlaySpace will not create a meeting for your appointments and will not return join links for them."
          example: None
        branding:
          description: White-label branding for the session surface (display name, logo, colours).
          example:
            display_name: Northwind Charts
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties: {}
            - type: "null"
        waiting_room_enabled:
          description: Whether a patient waits for the clinician to admit them before the session opens.
          example: true
          type: boolean
        default_camera_on:
          description: Whether cameras start on when a session opens.
          example: true
          type: boolean
        recording_enabled:
          description: Whether sessions may be recorded.
          example: false
          type: boolean
        upgrade_enabled:
          description: Whether your framed PlaySpace workspace offers the "Upgrade to PlaySpace" page to eligible clinic owners (the owner of a clinic, or its only practitioner, while the clinic has not finished upgrading). Off unless you turn it on.
          example: true
          type: boolean
      additionalProperties: false
      title: PartnerSessionConfigPatch
      description: The configuration fields to change. Every field is optional; anything you omit is left exactly as it was.
    PartnerStorybookCreateBody:
      type: object
      properties:
        prompt:
          type: string
          minLength: 10
          maxLength: 500
          description: What the story should be about, in the clinician's own words. Written into the book, so keep it to what the story needs.
          example: A brave fox who learns to ask a grown-up for help when they feel worried
        settings:
          $ref: "#/components/schemas/PartnerStorybookGenerationSettings"
      required:
        - prompt
        - settings
      additionalProperties: false
      title: PartnerStorybookCreateBody
      description: What to generate a storybook about, and how it should read and look.
    PartnerStorybookGenerationSettings:
      type: object
      properties:
        target_age:
          type: string
          enum:
            - 1-3
            - 3-6
            - 6-12
            - 12-18
            - 18-40
            - 40-65
            - 65+
          description: Age band the story is written for.
          example: 6-12
        number_of_pages:
          type: string
          enum:
            - few
            - short
            - medium
            - long
          description: Roughly how long the book should be.
          example: short
        lines_per_page:
          type: string
          enum:
            - brief
            - standard
            - detailed
            - extended
          description: How much text sits on each page.
          example: standard
        style:
          type: string
          enum:
            - educational
            - adventure
            - emotional
            - funny
          description: Tone of the story.
          example: emotional
        image_style:
          type: string
          enum:
            - cartoon
            - watercolor
            - storybook
            - modern
            - anime
          description: Look of the illustrations.
          example: watercolor
        include_character_names:
          description: Names to use for the characters. Whatever you send appears in the finished book.
          type: array
          items:
            type: string
      required:
        - target_age
        - number_of_pages
        - lines_per_page
        - style
        - image_style
      additionalProperties: false
      title: PartnerStorybookGenerationSettings
      description: How the story should be written and illustrated.
    PartnerStorybookUpdateBody:
      type: object
      properties:
        shelf_ready:
          type: boolean
          const: true
          description: "Set to `true` to make this storybook eligible for a playroom or toolkit shelf, which is what an attach requires. Only `true` is accepted: withdrawing a book also removes it from every shelf it is on and cannot restore what it was before, so it is not offered as the opposite of this flag."
          example: true
      required:
        - shelf_ready
      additionalProperties: false
      title: PartnerStorybookUpdateBody
      description: What to change about a storybook. Today the only change on offer is making it shelf-eligible.
    PartnerContentShareBody:
      type: object
      properties:
        patient_id:
          type: string
          pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
          format: uuid
          description: Patient to email the link to, as returned on the patients endpoints as `data[].id`. Must be on the acting practitioner's current roster.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        expiry_seconds:
          description: How long the emailed link stays usable, between 1 hour and 30 days. Defaults to 7 days.
          example: 604800
          type: integer
          minimum: 3600
          maximum: 2592000
        message:
          description: Optional note from the practitioner, included in the email above the link. Plain text, up to 500 characters.
          example: We read this one together today. Have a look before our next session.
          type: string
          minLength: 1
          maxLength: 500
      required:
        - patient_id
      additionalProperties: false
      title: PartnerContentShareBody
      description: "Which patient to email a link to, how long the link should work, and an optional note. There is no recipient field: the address comes from the patient record."
    PartnerToolkitCreateBody:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          description: Toolkit name shown in the picker. Trimmed and non-empty, max 200 characters.
          example: Anxiety Toolkit
        description:
          description: Optional note about when to use the toolkit. Max 1000 characters.
          example: Grounding activities for the middle of a session.
          type: string
          maxLength: 1000
        session_items:
          description: Items to enable in the toolkit. Omit to get the standard set. An empty array creates a toolkit with no activities.
          example:
            - activity_shelf
            - whiteboard
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
        tags:
          description: Optional labels, at most 20, each 1-50 characters. Defaults to none.
          example:
            - anxiety
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        published:
          default: true
          description: Whether the toolkit is immediately offered at session start. Defaults to true; send false to create it as a draft.
          example: true
          type: boolean
      required:
        - title
      additionalProperties: false
      title: PartnerToolkitCreateBody
      description: Body of `POST /v1/partner/toolkits`. Only `title` is required; the rest fall back to the same defaults the PlaySpace app applies. Unknown keys are rejected.
      example:
        title: Anxiety Toolkit
        session_items:
          - activity_shelf
          - whiteboard
        published: true
    PartnerToolkitUpdateBody:
      type: object
      properties:
        title:
          description: New toolkit name. Trimmed and non-empty, max 200 characters.
          example: Anxiety Toolkit
          type: string
          minLength: 1
          maxLength: 200
        description:
          description: New note, or null to clear it. Max 1000 characters.
          example: null
          anyOf:
            - type: string
              maxLength: 1000
            - type: "null"
        session_items:
          description: Replacement set of enabled items — the array replaces the current one entirely.
          example:
            - activity_shelf
            - sand_tray
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
        tags:
          description: Replacement labels — the array replaces the current ones entirely.
          example:
            - anxiety
            - intake
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        published:
          description: Publish (`true`) or unpublish (`false`) the toolkit.
          example: false
          type: boolean
      additionalProperties: false
      title: PartnerToolkitUpdateBody
      description: Body of `PATCH /v1/partner/toolkits/{id}`. Any subset of the create fields; at least one is required. Array fields replace rather than merge. Unknown keys are rejected.
      example:
        title: Anxiety Toolkit
        published: false
    PartnerWorksheetCreateBody:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
          description: Title as it will appear in the clinician's library.
          example: Feelings Thermometer
        description:
          description: Optional longer description. Returned on every read of this worksheet.
          example: Scaling exercise used at the start of a session.
          type: string
          maxLength: 2000
        category:
          description: Optional free-text category to file the worksheet under.
          example: Emotional regulation
          type: string
          maxLength: 100
        tags:
          description: Optional tags, up to 20.
          example:
            - anxiety
            - scaling
          maxItems: 20
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 50
        pages:
          minItems: 1
          maxItems: 100
          type: array
          items:
            $ref: "#/components/schemas/PartnerWorksheetCreatePage"
          description: The worksheet's pages, in reading order. At least one. The whole set travels in this one request, so keep the encoded body under roughly 4MB — split a long document into several worksheets rather than one oversized request.
      required:
        - title
        - pages
      additionalProperties: false
      title: PartnerWorksheetCreateBody
      description: A worksheet to create in the acting practitioner's library, with its page images.
    PartnerWorksheetCreatePage:
      type: object
      properties:
        image_base64:
          type: string
          minLength: 1
          description: The page image, base64-encoded. PNG, JPEG, GIF or WebP — the format is detected from the bytes, so no separate type field is needed and a mislabelled one cannot mislead. Pages appear in the order given.
      required:
        - image_base64
      additionalProperties: false
      title: PartnerWorksheetCreatePage
      description: One page image of a worksheet being created.
    PartnerAppointmentListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerAppointment"
          description: Page of appointments.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerAppointmentListResponse
      description: Response envelope for `GET /v1/partner/appointments`. `data` is the page of appointments; `meta` carries the request id, generation time, and cursor pagination state.
    PartnerAppointment:
      type: object
      properties:
        video:
          $ref: "#/components/schemas/PartnerAppointmentVideoMetadata"
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of this appointment. Stable for the lifetime of the appointment and used on every subsequent call.
          example: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
        partner_clinic_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the clinic this appointment belongs to.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the practitioner this appointment is scheduled with.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the patient this appointment is for.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at:
          type: string
          format: date-time
          description: Appointment start as an ISO-8601 offset-aware datetime. This is an absolute instant; read the `timezone` field to render it as local wall-clock time.
          example: 2026-06-02T15:00:00.000Z
        end_at:
          type: string
          format: date-time
          description: Appointment end as an ISO-8601 offset-aware datetime. Always strictly after `start_at`.
          example: 2026-06-02T15:50:00.000Z
        timezone:
          type: string
          description: IANA timezone identifier (e.g. `America/New_York`) used to render `start_at` / `end_at` as local wall-clock time. Defaults to `UTC` when none is set. On read, older appointments may occasionally carry a non-IANA value.
          example: America/New_York
        session_type:
          type: string
          enum:
            - virtual
            - in_person
          description: "Session delivery mode: `virtual` (video) or `in_person`. A `virtual` appointment carries a patient join link on `patient_video_url`, unless your organization is configured with `video_provider` set to `None`."
          example: virtual
        patient_video_url:
          anyOf:
            - type: string
            - type: "null"
          description: Patient PlaySpace entry link, never a supplied provider URL. Null for in-person or unprovisioned appointments, and platform-video appointments when the organization selects None. Partner-video appointments keep both PlaySpace entry links even with None. This link grants session access; deliver it securely to the patient.
          example: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        status:
          type: string
          enum:
            - scheduled
            - completed
            - cancelled
            - no_show
          description: Appointment status, one of `scheduled`, `completed`, `cancelled`, or `no_show`. A late cancellation reports as `cancelled`; a non-billable session that took place reports as `completed`.
          example: scheduled
        notes:
          anyOf:
            - type: string
            - type: "null"
          description: Free-text appointment notes, or `null` when none are set. May contain protected health information, so handle and store it accordingly.
          example: Initial intake session.
        created_at:
          type: string
          description: ISO-8601 timestamp recording when this appointment was booked through the API.
          example: 2026-06-01T12:00:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp recording when this appointment was last updated.
          example: 2026-06-01T12:00:00.000Z
        deleted_at:
          description: ISO-8601 timestamp recording when this appointment was cancelled, or `null` when active. Only returned when the request sets `include_deleted=true`; otherwise the field is omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - video
        - id
        - partner_clinic_id
        - partner_practitioner_id
        - partner_patient_id
        - start_at
        - end_at
        - timezone
        - session_type
        - patient_video_url
        - status
        - notes
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerAppointment
      description: An appointment linking one practitioner and one patient. `start_at` / `end_at` / `timezone` / `session_type` / `status` / `patient_video_url` describe the scheduled session; `created_at` / `updated_at` / `deleted_at` track when the appointment was booked, last changed, and cancelled. The `notes` field may contain protected health information.
      example:
        id: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at: 2026-06-02T15:00:00.000Z
        end_at: 2026-06-02T15:50:00.000Z
        timezone: America/New_York
        session_type: virtual
        patient_video_url: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        status: scheduled
        notes: Initial intake session.
        created_at: 2026-06-01T12:00:00.000Z
        updated_at: 2026-06-01T12:00:00.000Z
    PartnerAppointmentVideoMetadata:
      oneOf:
        - type: object
          properties:
            ownership:
              type: string
              const: platform
          required:
            - ownership
          additionalProperties: false
        - type: object
          properties:
            ownership:
              type: string
              const: partner
            label:
              type: string
            presentation:
              type: string
              enum:
                - inline
                - external
            expires_at:
              type: string
          required:
            - ownership
            - presentation
          additionalProperties: false
      description: Video ownership and display metadata. Supplied provider URLs are never returned on appointment reads; each participant receives only their own URL when joining PlaySpace.
      type: object
    EnvelopeMeta:
      type: object
      properties:
        request_id:
          type: string
          minLength: 1
          description: UUID for this request. Also echoed in the X-Request-Id response header.
        generated_at:
          type: string
          description: ISO-8601 timestamp at which the response was produced.
          example: 2026-05-22T15:30:00.000Z
        pagination:
          description: Present on list endpoints; omitted on single-resource endpoints.
          $ref: "#/components/schemas/PaginationMeta"
      required:
        - request_id
        - generated_at
      additionalProperties: false
      title: EnvelopeMeta
      description: Standard meta block emitted in the success envelope on every 2xx response.
    PaginationMeta:
      type: object
      properties:
        cursor:
          description: Cursor used for the current page; omitted on the first page.
          type: string
        next_cursor:
          description: Cursor for the next page, or null at the end of the list.
          anyOf:
            - type: string
            - type: "null"
        limit:
          type: integer
          minimum: 1
          maximum: 100
          description: Page size requested by the caller, post-validation.
          example: 25
        has_more:
          type: boolean
          description: True if another page exists after this one.
      required:
        - limit
        - has_more
      additionalProperties: false
      title: PaginationMeta
      description: Cursor-pagination metadata returned in meta.pagination for list endpoints.
    Problem:
      type: object
      properties:
        type:
          type: string
          format: uri
          description: Absolute URI identifying the problem type. All partner-API problem types live under https://api.playspace.health/problems/<slug>. The URI is a stable identifier and is not guaranteed to resolve.
          example: https://api.playspace.health/problems/unauthorized
        title:
          type: string
          minLength: 1
          description: Short, human-readable summary of the problem type.
          example: Unauthorized
        status:
          type: integer
          minimum: 400
          maximum: 599
          description: HTTP status code for this occurrence.
          example: 401
        detail:
          description: Human-readable explanation specific to this occurrence.
          type: string
        instance:
          description: URI reference identifying the specific occurrence — typically the request path.
          type: string
      required:
        - type
        - title
        - status
      additionalProperties: {}
      title: Problem
      description: RFC 9457 Problem Details object. Returned with content-type application/problem+json for every error response.
    PartnerAppointmentWriteResponse:
      type: object
      properties:
        data:
          description: The created or updated appointment.
          $ref: "#/components/schemas/PartnerAppointmentWrite"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerAppointmentWriteResponse
      description: Response envelope for the appointment write responses (`POST` 201, `PATCH` 200). `data` is the created or updated appointment including `clinician_video_url`; `meta` carries the request id and generation time, with no pagination.
    PartnerAppointmentWrite:
      type: object
      properties:
        video:
          $ref: "#/components/schemas/PartnerAppointmentVideoMetadata"
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of this appointment. Stable for the lifetime of the appointment and used on every subsequent call.
          example: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
        partner_clinic_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the clinic this appointment belongs to.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the practitioner this appointment is scheduled with.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the patient this appointment is for.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at:
          type: string
          format: date-time
          description: Appointment start as an ISO-8601 offset-aware datetime. This is an absolute instant; read the `timezone` field to render it as local wall-clock time.
          example: 2026-06-02T15:00:00.000Z
        end_at:
          type: string
          format: date-time
          description: Appointment end as an ISO-8601 offset-aware datetime. Always strictly after `start_at`.
          example: 2026-06-02T15:50:00.000Z
        timezone:
          type: string
          description: IANA timezone identifier (e.g. `America/New_York`) used to render `start_at` / `end_at` as local wall-clock time. Defaults to `UTC` when none is set. On read, older appointments may occasionally carry a non-IANA value.
          example: America/New_York
        session_type:
          type: string
          enum:
            - virtual
            - in_person
          description: "Session delivery mode: `virtual` (video) or `in_person`. A `virtual` appointment carries a patient join link on `patient_video_url`, unless your organization is configured with `video_provider` set to `None`."
          example: virtual
        patient_video_url:
          anyOf:
            - type: string
            - type: "null"
          description: Patient PlaySpace entry link, never a supplied provider URL. Null for in-person or unprovisioned appointments, and platform-video appointments when the organization selects None. Partner-video appointments keep both PlaySpace entry links even with None. This link grants session access; deliver it securely to the patient.
          example: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        status:
          type: string
          enum:
            - scheduled
            - completed
            - cancelled
            - no_show
          description: Appointment status, one of `scheduled`, `completed`, `cancelled`, or `no_show`. A late cancellation reports as `cancelled`; a non-billable session that took place reports as `completed`.
          example: scheduled
        notes:
          anyOf:
            - type: string
            - type: "null"
          description: Free-text appointment notes, or `null` when none are set. May contain protected health information, so handle and store it accordingly.
          example: Initial intake session.
        created_at:
          type: string
          description: ISO-8601 timestamp recording when this appointment was booked through the API.
          example: 2026-06-01T12:00:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp recording when this appointment was last updated.
          example: 2026-06-01T12:00:00.000Z
        deleted_at:
          description: ISO-8601 timestamp recording when this appointment was cancelled, or `null` when active. Only returned when the request sets `include_deleted=true`; otherwise the field is omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
        clinician_video_url:
          anyOf:
            - type: string
            - type: "null"
          description: Clinician (host) join link for a virtual appointment, or `null` for in-person appointments and when no video session was provisioned. Like `patient_video_url` it grants session access (it redirects to a credential-bearing session URL), so treat it as sensitive and deliver it only to the assigned clinician over a secure channel. Returned only on create and reschedule responses.
          example: https://app.playspace.health/join/Zq7wX4vT1sR8nM5kJ2hG0f
      required:
        - video
        - id
        - partner_clinic_id
        - partner_practitioner_id
        - partner_patient_id
        - start_at
        - end_at
        - timezone
        - session_type
        - patient_video_url
        - status
        - notes
        - created_at
        - updated_at
        - clinician_video_url
      additionalProperties: false
      title: PartnerAppointmentWrite
      description: "A created or updated appointment: identical to `PartnerAppointment` plus `clinician_video_url`, the practitioner's own join link for a virtual session. Returned only on create and reschedule responses, never on reads."
      example:
        id: e2f3a4b5-6c7d-4e8f-9a0b-1c2d3e4f5a6b
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        partner_practitioner_id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_patient_id: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        start_at: 2026-06-02T15:00:00.000Z
        end_at: 2026-06-02T15:50:00.000Z
        timezone: America/New_York
        session_type: virtual
        patient_video_url: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        clinician_video_url: https://app.playspace.health/join/Zq7wX4vT1sR8nM5kJ2hG0f
        status: scheduled
        notes: Initial intake session.
        created_at: 2026-06-01T12:00:00.000Z
        updated_at: 2026-06-01T12:00:00.000Z
    EnvelopeMetaWithoutPagination:
      type: object
      properties:
        request_id:
          type: string
          minLength: 1
          description: UUID for this request. Also echoed in the X-Request-Id response header.
        generated_at:
          type: string
          description: ISO-8601 timestamp at which the response was produced.
          example: 2026-05-22T15:30:00.000Z
      required:
        - request_id
        - generated_at
      additionalProperties: false
      title: EnvelopeMetaWithoutPagination
      description: Meta block emitted in the success envelope on every 2xx single-resource response (GET-by-id, POST, PATCH, DELETE). No pagination field — pagination is reserved for list endpoints.
    PartnerAppointmentResponse:
      type: object
      properties:
        data:
          description: The appointment at the requested id.
          $ref: "#/components/schemas/PartnerAppointment"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerAppointmentResponse
      description: Response envelope for the single-appointment read response (`GET` by id). `data` is a single appointment; `meta` carries the request id and generation time, with no pagination.
    PartnerAppointmentSessionLinksResponse:
      type: object
      properties:
        data:
          description: The join links for the appointment.
          $ref: "#/components/schemas/PartnerAppointmentSessionLinks"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerAppointmentSessionLinksResponse
      description: Response envelope for `POST /v1/partner/appointments/{id}/session-links`. `data` carries `patient_video_url` / `clinician_video_url`; `meta` carries the request id and generation time, with no pagination.
    PartnerAppointmentSessionLinks:
      type: object
      properties:
        patient_video_url:
          anyOf:
            - type: string
            - type: "null"
          description: Patient join link for the appointment; the same link on every call. Always `null` for an in-person appointment, where the second seat is never taken; for a virtual appointment, `null` only if the session could not be provisioned. Grants session access (it redirects to a credential-bearing session URL), so treat it as sensitive and deliver it to the patient over a secure channel.
          example: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        clinician_video_url:
          anyOf:
            - type: string
            - type: "null"
          description: Clinician (host) entry link for the appointment, the same on every call, for both virtual and in-person appointments. `null` only if the session could not be provisioned. Grants session access (it redirects to a credential-bearing session URL), so treat it as sensitive and deliver it only to the assigned clinician over a secure channel.
          example: https://app.playspace.health/join/Zq7wX4vT1sR8nM5kJ2hG0f
        video_provider:
          type: string
          enum:
            - Whereby
            - Dailyco
            - EightxEight
            - None
          description: Which video service these links open into. `None` means the session shows the playroom only and the links carry no video call — either because your organization supplies video separately, or because this appointment is in-person. Otherwise configured for your organization by PlaySpace.
          example: None
        waiting_room_enabled:
          type: boolean
          description: Whether the patient waits on a holding screen until the clinician admits them. Always `false` for an in-person appointment, which has no second seat to admit. Otherwise only applies when `video_provider` is `None`; the other services hold the patient in their own waiting room instead.
          example: true
      required:
        - patient_video_url
        - clinician_video_url
        - video_provider
        - waiting_room_enabled
      additionalProperties: false
      title: PartnerAppointmentSessionLinks
      description: "The role-scoped session entry links for an appointment, the same on every call: `patient_video_url` for the patient (virtual appointments only) and `clinician_video_url` for the assigned clinician, alongside what those links will open into."
      example:
        patient_video_url: https://app.playspace.health/join/aB3dE6fG9hJ2kL5mN8pQ1r
        clinician_video_url: https://app.playspace.health/join/Zq7wX4vT1sR8nM5kJ2hG0f
        video_provider: None
        waiting_room_enabled: true
    ClinicCommunityListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/ClinicCommunityItem"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: ClinicCommunityListResponse
      description: Envelope for GET /v1/partner/clinic-community/items. meta.pagination carries the cursor block.
    ClinicCommunityItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Clinic-community item id. Use it to preview, copy or remove the item.
        item_type:
          $ref: "#/components/schemas/ClinicCommunityItemType"
        item_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The shared copy's own id in its resource. Read-only; the copy belongs to the clinic community, not to any library.
        title:
          type: string
          description: Title as shown to colleagues. Clinician-authored text.
          example: Feelings thermometer
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Note for colleagues, or null. Clinician-authored text.
        age_categories:
          type: array
          items:
            type: string
            enum:
              - child
              - teen
              - adult
          description: Who it suits. Empty when unspecified, never null.
        tags:
          type: array
          items:
            type: string
          description: Tags. Empty when none, never null.
          example:
            - anxiety
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null (forms have none). Expires; re-list rather than caching it.
        copy_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many times colleagues have copied this item into their own library.
          example: 2
        shared_by_practitioner_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: The practitioner who shared it, as their id on the practitioners endpoints, or null when the sharer is not on your roster.
        shared_by_me:
          type: boolean
          description: True when the acting practitioner shared this item. Always false on an organization token.
        created_at:
          type: string
          description: ISO-8601 instant the item was shared.
        updated_at:
          type: string
          description: ISO-8601 last-update instant.
      required:
        - id
        - item_type
        - item_id
        - title
        - description
        - age_categories
        - tags
        - thumbnail_url
        - copy_count
        - shared_by_practitioner_id
        - shared_by_me
        - created_at
        - updated_at
      additionalProperties: false
      title: ClinicCommunityItem
      description: One item shared with the clinic.
    ClinicCommunityDetailResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/ClinicCommunityItemDetail"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: ClinicCommunityDetailResponse
      description: Envelope for a single clinic-community item with its content.
    ClinicCommunityItemDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Clinic-community item id. Use it to preview, copy or remove the item.
        item_type:
          $ref: "#/components/schemas/ClinicCommunityItemType"
        item_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The shared copy's own id in its resource. Read-only; the copy belongs to the clinic community, not to any library.
        title:
          type: string
          description: Title as shown to colleagues. Clinician-authored text.
          example: Feelings thermometer
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Note for colleagues, or null. Clinician-authored text.
        age_categories:
          type: array
          items:
            type: string
            enum:
              - child
              - teen
              - adult
          description: Who it suits. Empty when unspecified, never null.
        tags:
          type: array
          items:
            type: string
          description: Tags. Empty when none, never null.
          example:
            - anxiety
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null (forms have none). Expires; re-list rather than caching it.
        copy_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many times colleagues have copied this item into their own library.
          example: 2
        shared_by_practitioner_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: The practitioner who shared it, as their id on the practitioners endpoints, or null when the sharer is not on your roster.
        shared_by_me:
          type: boolean
          description: True when the acting practitioner shared this item. Always false on an organization token.
        created_at:
          type: string
          description: ISO-8601 instant the item was shared.
        updated_at:
          type: string
          description: ISO-8601 last-update instant.
        content:
          anyOf:
            - $ref: "#/components/schemas/PartnerWorksheetDetail"
            - $ref: "#/components/schemas/PartnerStorybookDetail"
            - $ref: "#/components/schemas/PartnerForm"
          description: The shared content, in the same shape as the matching get-by-id endpoint for its kind, discriminated by `item_type`. Page text and page images are clinician-authored; render them, do not log them.
      required:
        - id
        - item_type
        - item_id
        - title
        - description
        - age_categories
        - tags
        - thumbnail_url
        - copy_count
        - shared_by_practitioner_id
        - shared_by_me
        - created_at
        - updated_at
        - content
      additionalProperties: false
      title: ClinicCommunityItemDetail
      description: One shared item with its content.
    PartnerWorksheetDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace worksheet id. Use it to open the worksheet in an embedded viewer.
        title:
          type: string
          description: The worksheet's title as it appears in the clinician's library.
          example: Feelings Thermometer
        page_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of pages in the worksheet.
          example: 3
        shelf_ready:
          type: boolean
          description: "Read-only and derived: true when this worksheet is ELIGIBLE to be placed on a playroom or toolkit shelf — that is, when an attach will accept this id. Eligibility is not placement: an eligible worksheet may be on no shelf at all, and a worksheet already on one stops being eligible if its publication state changes. Attaching an id whose `shelf_ready` is false is refused with a 422 carrying `reason: not_shelf_ready`, so read this before attaching rather than after."
          example: true
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null when none exists. Expires — re-list rather than caching it.
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        description:
          anyOf:
            - type: string
            - type: "null"
          description: The worksheet's description, or null when it has none. Clinician-authored library text.
          example: Scaling exercise used at the start of a session.
        category:
          anyOf:
            - type: string
            - type: "null"
          description: Free-text category the worksheet is filed under, or null when it has none.
          example: Emotional regulation
        tags:
          type: array
          items:
            type: string
          description: Tags on the worksheet. Empty when it has none — never null.
          example:
            - anxiety
            - scaling
        deleted_at:
          description: ISO-8601 timestamp at which the worksheet was deleted, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
        pages:
          type: array
          items:
            $ref: "#/components/schemas/PartnerWorksheetPage"
          description: Pages in reading order. Empty when the worksheet has no pages.
      required:
        - id
        - title
        - page_count
        - shelf_ready
        - thumbnail_url
        - created_at
        - updated_at
        - description
        - category
        - tags
        - pages
      additionalProperties: false
      title: PartnerWorksheetDetail
      description: One worksheet with its pages.
    PartnerWorksheetPage:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Page id.
        position:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Reading order, starting at 1.
          example: 1
        text:
          anyOf:
            - type: string
            - type: "null"
          description: Text on this page, or null when the page carries only artwork. Clinician-authored content.
        image_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the page background, or null when the page has none or the link could not be issued. Expires — re-read rather than caching it. Anything drawn on top of the background is baked into the PDF, not into this image.
      required:
        - id
        - position
        - text
        - image_url
      additionalProperties: false
      title: PartnerWorksheetPage
      description: One page of a worksheet, in reading order.
    PartnerStorybookDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace storybook id. Use it to open the book in an embedded reader.
        title:
          type: string
          description: Model-generated title. May echo details from the clinician prompt — treat as patient-adjacent content.
          example: Finn the Brave Fox Asks for Help
        status:
          type: string
          description: Generation state. `ready` books are readable; `generating` ones are still being illustrated; `failed` ones will not complete.
          example: ready
        page_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of pages currently in the book.
          example: 4
        shelf_ready:
          type: boolean
          description: "Read-only and derived: true when this storybook is ELIGIBLE to be placed on a playroom or toolkit shelf — that is, when an attach will accept this id. Eligibility is not placement: an eligible book may be on no shelf at all, and a book already on one stops being eligible if its publication state changes. Attaching an id whose `shelf_ready` is false is refused with a 422 carrying `reason: not_shelf_ready`, so read this before attaching rather than after."
          example: true
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null when none has been rendered yet. Expires — re-list rather than caching it.
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        deleted_at:
          description: ISO-8601 timestamp at which the storybook was deleted, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
        pages:
          type: array
          items:
            $ref: "#/components/schemas/PartnerStorybookPage"
          description: Pages in reading order. Empty while a book is still being generated, or when generation failed.
      required:
        - id
        - title
        - status
        - page_count
        - shelf_ready
        - thumbnail_url
        - created_at
        - updated_at
        - pages
      additionalProperties: false
      title: PartnerStorybookDetail
      description: One storybook with its pages.
    PartnerStorybookPage:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Page id.
        position:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Reading order, starting at 1.
          example: 1
        text:
          anyOf:
            - type: string
            - type: "null"
          description: Story text on this page, or null when the page carries only an illustration. Written for one child — clinical content.
        image_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the illustration, or null when the page has none or the link could not be issued. Expires — re-read rather than caching it.
      required:
        - id
        - position
        - text
        - image_url
      additionalProperties: false
      title: PartnerStorybookPage
      description: One page of a storybook, in reading order.
    PartnerForm:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace form id. Pass it to the embedded form surface to open this form for filling.
        name:
          type: string
          description: Clinician-authored form name.
          example: Intake questionnaire
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Optional longer description, or null when the author set none.
        embeddable:
          type: boolean
          description: Whether the embedded form surface can render this form. False when the form uses a field type the embed does not support yet (see unsupported_field_types) — the form is still listed so a host can explain the gap rather than silently omit it.
          example: true
        unsupported_field_types:
          type: array
          items:
            type: string
          description: Distinct field types in this form that the embed cannot render, empty when embeddable is true. Present so a host can say WHICH capability is missing.
          example:
            - signature
        shelf_ready:
          type: boolean
          description: "Read-only and derived: true when this form is ELIGIBLE to render on a playroom or toolkit shelf, so the clinician can also open it from inside a live PlaySpace session. Independent of appearing in this list: THIS list is gated on the authoring lifecycle (published, not draft), while shelf-readiness is gated on the clinician sharing the form to their playrooms and toolkits — and sharing is off by default, so a listed form that is not shelf-ready is the common case. Eligibility is not placement: actual placement is a separate membership row, written with PUT /v1/partner/{playrooms,toolkits}/{id}/content/form/{formId} exactly as for a worksheet or a storybook — an attach of a form whose shelf_ready is false is refused with 422 reason: not_shelf_ready, which is why reading this flag first is cheaper than discovering it through the failure. A host that only frames forms through the embed can ignore this; a host that coordinates with what happens in session should not assume a listed form is reachable there."
          example: false
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
      required:
        - id
        - name
        - description
        - embeddable
        - unsupported_field_types
        - shelf_ready
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerForm
      description: One published form owned by the acting practitioner. The field definition itself is deliberately NOT returned here — it is large, it changes independently of this list, and the embedded surface fetches it directly with the capability token.
    ClinicCommunityCopyResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/ClinicCommunityCopy"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: ClinicCommunityCopyResponse
      description: Envelope for POST /v1/partner/clinic-community/items/{id}/copy.
    ClinicCommunityCopy:
      type: object
      properties:
        item_type:
          $ref: "#/components/schemas/ClinicCommunityItemType"
        item_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The new copy's id in the acting practitioner's own library. Read it back on that resource's get-by-id endpoint.
      required:
        - item_type
        - item_id
      additionalProperties: false
      title: ClinicCommunityCopy
      description: The library copy a copy request made.
    PartnerClinicListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerClinic"
          description: Page of clinics.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerClinicListResponse
      description: Response envelope for `GET /v1/partner/clinics`. `data` is the page of clinics; `meta` carries the request id, timestamp, and cursor pagination state.
    PartnerClinic:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Stable UUID for the clinic. Use this id on every subsequent call that targets the clinic.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        name:
          type: string
          description: Clinic display name.
          example: Downtown Therapy Center
        timezone:
          type: string
          description: IANA timezone for the clinic. Applied to practitioners created under the clinic.
          example: America/New_York
        created_at:
          type: string
          description: ISO-8601 timestamp at which the clinic was created.
          example: 2026-05-22T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp at which the clinic was last updated.
          example: 2026-05-22T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp at which the clinic was de-listed, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - name
        - timezone
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerClinic
      description: "A clinic as seen by the partner: a stable id, display name, and timezone."
      example:
        id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        name: Downtown Therapy Center
        timezone: America/New_York
        created_at: 2026-05-22T15:30:00.000Z
        updated_at: 2026-05-22T15:30:00.000Z
    PartnerClinicResponse:
      type: object
      properties:
        data:
          description: The clinic at the requested id.
          $ref: "#/components/schemas/PartnerClinic"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerClinicResponse
      description: Response envelope for a single clinic. `data` is one clinic; `meta` carries the request id and timestamp, with no pagination.
    ClinicNotEmptyProblem:
      type: object
      properties:
        type:
          type: string
          format: uri
          description: Absolute URI identifying the problem type. All partner-API problem types live under https://api.playspace.health/problems/<slug>. The URI is a stable identifier and is not guaranteed to resolve.
          example: https://api.playspace.health/problems/unauthorized
        title:
          type: string
          minLength: 1
          description: Short, human-readable summary of the problem type.
          example: Unauthorized
        status:
          type: integer
          minimum: 400
          maximum: 599
          description: HTTP status code for this occurrence.
          example: 401
        detail:
          description: Human-readable explanation specific to this occurrence.
          type: string
        instance:
          description: URI reference identifying the specific occurrence — typically the request path.
          type: string
        practitioner_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Number of active practitioners still belonging to this clinic. The clinic cannot be deleted while this is greater than 0; clear or reassign the practitioners first.
          example: 2
        patient_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Number of active patients still belonging to this clinic. At v1 this always reads 0; the key is reserved so it can begin blocking deletion without a breaking contract change.
          example: 0
        appointment_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Number of active appointments still belonging to this clinic. At v1 this always reads 0; the key is reserved so it can begin blocking deletion without a breaking contract change.
          example: 0
      required:
        - type
        - title
        - status
        - practitioner_count
        - patient_count
        - appointment_count
      additionalProperties: {}
      title: ClinicNotEmptyProblem
      description: Problem-details body for the `clinic-not-empty` 409 returned by DELETE /v1/partner/clinics/{id}. Extends the base Problem object with practitioner_count, patient_count, and appointment_count — non-negative integers naming how many dependents still belong to the clinic. At v1, patient_count and appointment_count always read 0; the keys are reserved so deletion can begin blocking on patients and appointments without a breaking contract change.
    PartnerEmbedTokenResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerEmbedToken"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerEmbedTokenResponse
      description: Top-level envelope for POST /v1/partner/embed-tokens.
    PartnerEmbedToken:
      type: object
      properties:
        token:
          type: string
          description: "The embed capability token. Place it in the iframe URL. Treat as a credential: it is bearer-equivalent for the capabilities it names."
        expires_at:
          type: string
          description: ISO-8601 instant after which the token is rejected.
          example: 2026-08-12T15:45:00.000Z
        practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace practitioner the embed will act as, resolved from the delegated token. Storybooks created in the frame are owned by this practitioner.
        partner_practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The partner-namespace practitioner id the token was delegated to — the identifier the partner already holds.
        patient_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: The patient the token is bound to, echoed back, or null when none was requested. This is the same PlaySpace patient id that was sent in — the internal client identifier it maps to is never returned.
        capabilities:
          type: array
          items:
            $ref: "#/components/schemas/EmbedCapability"
          description: Capabilities actually granted, echoed for confirmation.
      required:
        - token
        - expires_at
        - practitioner_id
        - partner_practitioner_id
        - patient_id
        - capabilities
      additionalProperties: false
      title: PartnerEmbedToken
      description: A minted embed capability token and its binding.
    PartnerExportResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerExport"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerExportResponse
      description: Top-level envelope for the export start and status endpoints. Both return this identical body.
    PartnerExport:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace export id. Poll and download using this.
        status:
          type: string
          enum:
            - queued
            - running
            - assembling
            - ready
            - failed
            - expired
          description: Where the export has got to. `queued`, `running` and `assembling` all mean "keep waiting". `ready` means the archive can be downloaded. `failed` means it will never be downloadable — read `failed_step` and `error_message`, then start a new one. `expired` means it finished but its files have since been deleted.
          example: queued
        counts:
          $ref: "#/components/schemas/PartnerExportCounts"
        archive:
          $ref: "#/components/schemas/PartnerExportArchive"
        expires_at:
          anyOf:
            - type: string
            - type: "null"
          description: ISO-8601 instant after which the archive is deleted and the download stops working — seven days after it became ready. Null while the export is still running, because nothing has been published yet.
          example: 2026-09-16T09:14:22.000Z
        created_at:
          type: string
          description: ISO-8601 instant the export was requested.
          example: 2026-09-09T09:14:22.000Z
        failed_step:
          anyOf:
            - type: string
            - type: "null"
          description: Which phase failed, when the status is `failed`; null otherwise. A short internal phase name, useful to quote to support.
          example: null
        error_message:
          anyOf:
            - type: string
            - type: "null"
          description: A one-sentence, human-readable reason the export failed, when the status is `failed`; null otherwise. Safe to show to the clinician — it never carries record content.
          example: null
      required:
        - id
        - status
        - counts
        - archive
        - expires_at
        - created_at
        - failed_step
        - error_message
      additionalProperties: false
      title: PartnerExport
      description: One export of the acting clinician's records. The same shape is returned when an export is started and every time it is polled, so one parser covers both.
    PartnerExportCounts:
      type: object
      properties:
        total_steps:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many phases this export will run in total. `0` until the export has worked out what it has to collect, which is why progress should be read as a fraction rather than as a countdown.
          example: 12
        completed_steps:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many of those phases have finished. Divide by `total_steps` for a progress percentage, and treat a `total_steps` of `0` as "not known yet" rather than as complete.
          example: 4
        table_rows:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many records were gathered into the archive. Zero until collection has run.
          example: 1840
        documents:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many documents — notes, forms, worksheets, storybooks, uploads — were rendered into the archive.
          example: 96
        skipped:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: How many items were deliberately left out. Every one of them is listed with a reason in the archive itself, so a non-zero value here is a pointer to that list rather than a failure.
          example: 2
      required:
        - total_steps
        - completed_steps
        - table_rows
        - documents
        - skipped
      additionalProperties: false
      title: PartnerExportCounts
      description: Progress and volume counters for one export. All five start at zero and only rise; none of them is a promise about the finished archive until the status is `ready`.
    PartnerExportArchive:
      type: object
      properties:
        byte_size:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Size of the finished archive in bytes, or null before it has been packed. Check the file you download against this.
          example: 48211904
        sha256:
          anyOf:
            - type: string
            - type: "null"
          description: Lowercase hex SHA-256 of the finished archive, or null before it has been packed. This and `byte_size` are the only integrity claim the archive makes — verify both after downloading, because a truncated transfer is otherwise indistinguishable from a complete one.
          example: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
      required:
        - byte_size
        - sha256
      additionalProperties: false
      title: PartnerExportArchive
      description: What the finished archive weighs and hashes to. Both fields are null until the export has been packed; the object itself is always present.
    PartnerFormListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerForm"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerFormListResponse
      description: Top-level envelope for GET /v1/partner/forms. meta.pagination carries the cursor block.
    PartnerFormResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerForm"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerFormResponse
      description: Top-level envelope for the single-form endpoints. No pagination block.
    PartnerFormSubmissionListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerFormSubmission"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerFormSubmissionListResponse
      description: Top-level envelope for the submission list. meta.pagination carries the cursor block.
    PartnerFormSubmission:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace submission id.
        form_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The form this answers — always the id in the path.
        patient_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: YOUR patient id, mapped back from PlaySpace's internal one. Null when the submission was recorded without a patient attached, or when the person answering has never been registered through this API.
        data:
          type: object
          propertyNames:
            type: string
          additionalProperties: {}
          description: The answers, keyed by field id. THIS IS PATIENT-AUTHORED CLINICAL CONTENT — render it, do not log it, and do not put it anywhere your own error reporting will capture.
        status:
          anyOf:
            - type: string
            - type: "null"
          description: "`completed` for a finished submission, `in_progress` for a saved draft. A draft is a real row and will appear here; check this before treating an answer set as final."
        submitted_by:
          anyOf:
            - type: string
            - type: "null"
          description: "Who the platform recorded as the author: `client` when the patient filled it themselves in a live session, `practitioner` when a clinician recorded it — including through an embedded form, where the capability token authorises the clinician even if the patient is the one typing."
          example: client
        submission_method:
          anyOf:
            - type: string
            - type: "null"
          description: "`in_platform` or `sent_out`."
        submitted_at:
          type: string
          description: ISO-8601 submission timestamp.
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
      required:
        - id
        - form_id
        - patient_id
        - data
        - status
        - submitted_by
        - submission_method
        - submitted_at
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerFormSubmission
      description: One set of answers recorded against a form. Contains patient-authored clinical content.
    PartnerFormSubmissionResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerFormSubmission"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerFormSubmissionResponse
      description: Top-level envelope for a single submission.
    PartnerGameSaveListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerGameSave"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerGameSaveListResponse
      description: Top-level envelope for GET /v1/partner/game-saves. meta.pagination carries the cursor block.
    PartnerGameSave:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace id of the saved scene.
        game_type:
          $ref: "#/components/schemas/PartnerGameType"
        save_name:
          anyOf:
            - type: string
            - type: "null"
          description: Name the clinician gave the scene, or null when it was never named. Clinician free text — may reference the child.
          example: Session 3 - the flood
        patient_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: Your patient id for the child the scene belongs to. Null when the scene was built outside a patient context, or for a patient your organization did not create.
        has_thumbnail:
          type: boolean
          description: Whether a preview image exists for this scene. When true, fetch the bytes from the thumbnail endpoint; there is no image URL on this payload.
          example: false
        is_active:
          type: boolean
          description: False once the scene has been deleted. Deleted scenes appear only when you ask for them with `include_deleted=true`.
          example: true
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-save timestamp.
      required:
        - id
        - game_type
        - save_name
        - patient_id
        - has_thumbnail
        - is_active
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerGameSave
      description: One saved sandtray or dollhouse scene owned by the acting clinician.
    PartnerGameSaveDetailResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerGameSaveDetail"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerGameSaveDetailResponse
      description: Top-level envelope for GET /v1/partner/game-saves/{id}.
    PartnerGameSaveDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace id of the saved scene.
        game_type:
          $ref: "#/components/schemas/PartnerGameType"
        save_name:
          anyOf:
            - type: string
            - type: "null"
          description: Name the clinician gave the scene, or null when it was never named. Clinician free text — may reference the child.
          example: Session 3 - the flood
        patient_id:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: "null"
          description: Your patient id for the child the scene belongs to. Null when the scene was built outside a patient context, or for a patient your organization did not create.
        has_thumbnail:
          type: boolean
          description: Whether a preview image exists for this scene. When true, fetch the bytes from the thumbnail endpoint; there is no image URL on this payload.
          example: false
        is_active:
          type: boolean
          description: False once the scene has been deleted. Deleted scenes appear only when you ask for them with `include_deleted=true`.
          example: true
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-save timestamp.
        state_data:
          description: The scene itself — every object, its position, and the surface it sits on. Shape is the scene format the PlaySpace bundle reads and writes; treat it as opaque unless you are rendering it. Around a megabyte, which is why it is only on this response.
      required:
        - id
        - game_type
        - save_name
        - patient_id
        - has_thumbnail
        - is_active
        - created_at
        - updated_at
        - state_data
      additionalProperties: false
      title: PartnerGameSaveDetail
      description: One saved scene, including its contents.
    PartnerGameSessionResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerGameSession"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerGameSessionResponse
      description: Top-level envelope for POST /v1/partner/game-sessions.
    PartnerGameSession:
      type: object
      properties:
        game_session_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Key both seats join on. Unguessable and single-use — anyone holding it plus a valid token is in the same live scene, so pass it only to the two participants.
        game_type:
          $ref: "#/components/schemas/PartnerGameType"
        practitioner:
          description: The clinician seat. May start a new scene, load a previous one, save, rename and delete.
          $ref: "#/components/schemas/PartnerGameSessionSeat"
        patient:
          description: The patient seat. Plays in the scene the clinician has chosen; cannot save, rename or delete.
          $ref: "#/components/schemas/PartnerGameSessionSeat"
      required:
        - game_session_id
        - game_type
        - practitioner
        - patient
      additionalProperties: false
      title: PartnerGameSession
      description: A live sandtray or dollhouse session and both ways into it.
    PartnerGameSessionSeat:
      type: object
      properties:
        embed_url:
          type: string
          description: "Ready-to-frame URL for this seat, with the token already attached. Put it straight in an iframe src. Treat it as a credential: never log it or render it as text."
          example: https://app.playspace.health/embed/games/sandtray?token=eyJhbGciOi...
        token:
          type: string
          description: The embed token on its own, for hosts that build the frame URL themselves or refresh a live frame. Bearer-equivalent for this seat.
        expires_at:
          type: string
          description: ISO-8601 instant after which this seat's token is rejected.
          example: 2026-08-18T15:45:00.000Z
      required:
        - embed_url
        - token
        - expires_at
      additionalProperties: false
      title: PartnerGameSessionSeat
      description: One participant's way into the session.
    HealthResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/Health"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: HealthResponse
      description: Top-level envelope for GET /v1/partner/health. data carries the health payload; meta carries the standard request_id/generated_at envelope-meta block.
    Health:
      type: object
      properties:
        status:
          type: string
          const: ok
          description: Always the literal string "ok" when the endpoint returns 200.
          example: ok
        partner_organization_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: UUID of the partner organization resolved from the request token. Echoed back so the partner can confirm the token is bound to the expected tenant.
          example: a1b2c3d4-e5f6-7890-abcd-ef0123456789
        checked_at:
          type: string
          description: ISO-8601 timestamp at which the health check was evaluated.
          example: 2026-05-22T15:30:00.000Z
      required:
        - status
        - partner_organization_id
        - checked_at
      additionalProperties: false
      title: Health
      description: Payload returned by GET /v1/partner/health when the partner token authenticates successfully.
    PartnerNoteListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            anyOf:
              - $ref: "#/components/schemas/PartnerNoteDetail"
              - $ref: "#/components/schemas/PartnerNote"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerNoteListResponse
      description: Top-level envelope for GET /v1/partner/notes. meta.pagination carries the cursor block. Items carry `content` and `version_number` only when `include=content` was set.
    PartnerNoteDetail:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace note id.
        title:
          type: string
          description: "The note's title. Often derived from the appointment it belongs to, but for a note written without one it is derived from the opening words of the note itself — so treat it as clinical content, not as a label: render it, do not log it or put it in a URL."
          example: Session note
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Clinician-authored subtitle for the note, or null when it has none.
          example: null
        locked:
          type: boolean
          description: True when the clinician has locked the note against further editing. Locking is a clinical act — a locked note is the finalised record of the session.
          example: false
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        partner_appointment_id:
          anyOf:
            - type: string
            - type: "null"
          description: "The appointment this note belongs to, as `id` on your own appointment records, or null. Null does not mean the note is unattached: it means the note is not attached to an appointment you booked through this API — a note taken during an ad-hoc recording has no appointment at all."
          example: null
        partner_patient_id:
          anyOf:
            - type: string
            - type: "null"
          description: The patient the session was with, as `id` on your own patient records, or null when the note has no appointment of yours behind it. Read the patient record for identity — it is never repeated here.
          example: null
        partner_practitioner_id:
          anyOf:
            - type: string
            - type: "null"
          description: The clinician the appointment was booked for, as `id` on your own practitioner records. This is always the clinician your token is acting as, and is null only when the note has no appointment of yours behind it.
          example: null
        content:
          type: string
          description: "The written body of the note, as the clinician most recently saved it. Empty when the note has never been written to. This is clinical content: render and store it as such, and never put it in a log line, a URL or an error message."
          example: ""
        version_number:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Which revision of the note `content` came from. Rises each time the clinician saves. Null when the note has never been written to.
          example: 1
      required:
        - id
        - title
        - description
        - locked
        - created_at
        - updated_at
        - partner_appointment_id
        - partner_patient_id
        - partner_practitioner_id
        - content
        - version_number
      additionalProperties: false
      title: PartnerNoteDetail
      description: One clinical note with its written body.
    PartnerNote:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace note id.
        title:
          type: string
          description: "The note's title. Often derived from the appointment it belongs to, but for a note written without one it is derived from the opening words of the note itself — so treat it as clinical content, not as a label: render it, do not log it or put it in a URL."
          example: Session note
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Clinician-authored subtitle for the note, or null when it has none.
          example: null
        locked:
          type: boolean
          description: True when the clinician has locked the note against further editing. Locking is a clinical act — a locked note is the finalised record of the session.
          example: false
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        partner_appointment_id:
          anyOf:
            - type: string
            - type: "null"
          description: "The appointment this note belongs to, as `id` on your own appointment records, or null. Null does not mean the note is unattached: it means the note is not attached to an appointment you booked through this API — a note taken during an ad-hoc recording has no appointment at all."
          example: null
        partner_patient_id:
          anyOf:
            - type: string
            - type: "null"
          description: The patient the session was with, as `id` on your own patient records, or null when the note has no appointment of yours behind it. Read the patient record for identity — it is never repeated here.
          example: null
        partner_practitioner_id:
          anyOf:
            - type: string
            - type: "null"
          description: The clinician the appointment was booked for, as `id` on your own practitioner records. This is always the clinician your token is acting as, and is null only when the note has no appointment of yours behind it.
          example: null
      required:
        - id
        - title
        - description
        - locked
        - created_at
        - updated_at
        - partner_appointment_id
        - partner_patient_id
        - partner_practitioner_id
      additionalProperties: false
      title: PartnerNote
      description: One clinical note belonging to the practitioner the token is acting as, without its written body.
    PartnerNoteDetailResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerNoteWithTranscript"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerNoteDetailResponse
      description: Top-level envelope for GET /v1/partner/notes/{id}. `data.transcript` is present only when `include=transcript` was set.
    PartnerNoteWithTranscript:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace note id.
        title:
          type: string
          description: "The note's title. Often derived from the appointment it belongs to, but for a note written without one it is derived from the opening words of the note itself — so treat it as clinical content, not as a label: render it, do not log it or put it in a URL."
          example: Session note
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Clinician-authored subtitle for the note, or null when it has none.
          example: null
        locked:
          type: boolean
          description: True when the clinician has locked the note against further editing. Locking is a clinical act — a locked note is the finalised record of the session.
          example: false
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        partner_appointment_id:
          anyOf:
            - type: string
            - type: "null"
          description: "The appointment this note belongs to, as `id` on your own appointment records, or null. Null does not mean the note is unattached: it means the note is not attached to an appointment you booked through this API — a note taken during an ad-hoc recording has no appointment at all."
          example: null
        partner_patient_id:
          anyOf:
            - type: string
            - type: "null"
          description: The patient the session was with, as `id` on your own patient records, or null when the note has no appointment of yours behind it. Read the patient record for identity — it is never repeated here.
          example: null
        partner_practitioner_id:
          anyOf:
            - type: string
            - type: "null"
          description: The clinician the appointment was booked for, as `id` on your own practitioner records. This is always the clinician your token is acting as, and is null only when the note has no appointment of yours behind it.
          example: null
        content:
          type: string
          description: "The written body of the note, as the clinician most recently saved it. Empty when the note has never been written to. This is clinical content: render and store it as such, and never put it in a log line, a URL or an error message."
          example: ""
        version_number:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: Which revision of the note `content` came from. Rises each time the clinician saves. Null when the note has never been written to.
          example: 1
        transcript:
          description: "The verbatim session recording, in order, present only when you asked for it with `include=transcript`. Each entry is one speaker turn — every consecutive word from the same speaker, joined — not one word, so the array is readable as it stands and needs no re-assembly. Null means this note has no transcript: either the session was never recorded, or the note was written by hand, or the transcription pipeline has not finished — those are indistinguishable here, and none of them is an error. Still expect it to be large: turning words into turns removes rows, not speech, so a long session runs to hundreds of turns and hundreds of kilobytes of verbatim text."
          anyOf:
            - type: array
              items:
                $ref: "#/components/schemas/PartnerNoteTranscriptSegment"
            - type: "null"
      required:
        - id
        - title
        - description
        - locked
        - created_at
        - updated_at
        - partner_appointment_id
        - partner_patient_id
        - partner_practitioner_id
        - content
        - version_number
      additionalProperties: false
      title: PartnerNoteWithTranscript
      description: One clinical note with its written body, and its verbatim session transcript when `include=transcript` was set.
    PartnerNoteTranscriptSegment:
      type: object
      properties:
        speaker:
          anyOf:
            - type: string
            - type: "null"
          description: Who said this, as the diarisation labelled them — a name where the recording carried one, otherwise the raw speaker identifier the transcription assigned, and null when the segment could not be attributed to anyone. Labels are stable within one transcript and mean nothing across two.
          example: Speaker 1
        text:
          type: string
          description: "What was said, verbatim. This is the most sensitive field on the whole API: it is the unedited speech of a therapy session, including everything the clinician chose not to write down. Render and store it as clinical content, never log it, and never put it in a URL."
          example: ZTest spoken sentence.
        start_seconds:
          type: number
          description: Offset from the start of the recording, in seconds, at which this segment begins.
          example: 12.4
        end_seconds:
          type: number
          description: Offset from the start of the recording, in seconds, at which this segment ends. Equal to `start_seconds` when the source gave no end time.
          example: 15.1
      required:
        - speaker
        - text
        - start_seconds
        - end_seconds
      additionalProperties: false
      title: PartnerNoteTranscriptSegment
      description: One continuous stretch of speech from one speaker in a session recording.
    PartnerPatientListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPatient"
          description: Page of patient records.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPatientListResponse
      description: Envelope for GET /v1/partner/patients. `data` is the page of patients; `meta` carries the request id, generation time, and pagination state.
    PartnerPatient:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Stable identifier for this patient. Use it on every subsequent API call.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        partner_clinic_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: ID of the clinic this patient belongs to.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        created_by_partner_practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: ID of the practitioner who created this patient record. Always present; set at creation and not changed afterward.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        first_name:
          anyOf:
            - type: string
            - type: "null"
          description: Patient given name. May be null when not recorded.
          example: Avery
        last_name:
          anyOf:
            - type: string
            - type: "null"
          description: Patient family name. May be null when not recorded.
          example: Lee
        preferred_name:
          anyOf:
            - type: string
            - type: "null"
          description: The name the patient prefers to be addressed by, which often differs from their given name. Null when not recorded.
          example: Alex
        date_of_birth:
          anyOf:
            - type: string
            - type: "null"
          description: Patient date of birth as an ISO-8601 calendar date (YYYY-MM-DD, no time component). May be null when not recorded.
          example: 1998-03-14
        email:
          anyOf:
            - type: string
            - type: "null"
          description: Patient email address. Returned as stored, which may include legacy values that do not match a strict email format. May be null when not recorded.
          example: avery.lee@example.com
        phone_number:
          anyOf:
            - type: string
            - type: "null"
          description: Patient phone number, returned verbatim with no normalization (E.164, formatted, and free-form values all occur). May be null when not recorded.
          example: +1-555-0123
        timezone:
          anyOf:
            - type: string
            - type: "null"
          description: Patient IANA timezone (e.g. America/New_York), used by the appointments surface for slot offsets. Returned as stored, which may include legacy non-IANA values. May be null when not recorded.
          example: America/New_York
        created_at:
          type: string
          description: ISO-8601 timestamp at which this patient was added to the partner roster.
          example: 2026-05-28T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp at which this patient record was last updated.
          example: 2026-05-28T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp at which this patient was removed from the partner roster, or null when active. Only returned when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - partner_clinic_id
        - created_by_partner_practitioner_id
        - first_name
        - last_name
        - preferred_name
        - date_of_birth
        - email
        - phone_number
        - timezone
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerPatient
      description: A patient record. Carries identity and contact fields that are PHI. Address and additional demographic fields are not included in this version and may be added later without a breaking change.
      example:
        id: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        created_by_partner_practitioner_id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        first_name: Avery
        last_name: Lee
        preferred_name: Alex
        date_of_birth: 1998-03-14
        email: avery.lee@example.com
        phone_number: +1-555-0123
        timezone: America/New_York
        created_at: 2026-05-28T15:30:00.000Z
        updated_at: 2026-05-28T15:30:00.000Z
    PartnerPatientCreateResponse:
      type: object
      properties:
        data:
          description: The newly created patient record. Does not include `linked_partner_practitioner_ids`.
          $ref: "#/components/schemas/PartnerPatient"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPatientCreateResponse
      description: Envelope for POST /v1/partner/patients (201). `data` is the newly created patient; `meta` carries the request id and generation time.
    PartnerPatientResponse:
      type: object
      properties:
        data:
          anyOf:
            - $ref: "#/components/schemas/PartnerPatient"
            - $ref: "#/components/schemas/PartnerPatientWithLinkedPractitioners"
          description: The patient record at the requested id. Carries the `linked_partner_practitioner_ids` field only when `include_linked_practitioners=true`.
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPatientResponse
      description: Envelope for GET /v1/partner/patients/{id}. `data` is a single patient record, optionally carrying `linked_partner_practitioner_ids`; `meta` carries the request id and generation time.
    PartnerPatientWithLinkedPractitioners:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Stable identifier for this patient. Use it on every subsequent API call.
          example: d1c2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
        partner_clinic_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: ID of the clinic this patient belongs to.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        created_by_partner_practitioner_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: ID of the practitioner who created this patient record. Always present; set at creation and not changed afterward.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        first_name:
          anyOf:
            - type: string
            - type: "null"
          description: Patient given name. May be null when not recorded.
          example: Avery
        last_name:
          anyOf:
            - type: string
            - type: "null"
          description: Patient family name. May be null when not recorded.
          example: Lee
        preferred_name:
          anyOf:
            - type: string
            - type: "null"
          description: The name the patient prefers to be addressed by, which often differs from their given name. Null when not recorded.
          example: Alex
        date_of_birth:
          anyOf:
            - type: string
            - type: "null"
          description: Patient date of birth as an ISO-8601 calendar date (YYYY-MM-DD, no time component). May be null when not recorded.
          example: 1998-03-14
        email:
          anyOf:
            - type: string
            - type: "null"
          description: Patient email address. Returned as stored, which may include legacy values that do not match a strict email format. May be null when not recorded.
          example: avery.lee@example.com
        phone_number:
          anyOf:
            - type: string
            - type: "null"
          description: Patient phone number, returned verbatim with no normalization (E.164, formatted, and free-form values all occur). May be null when not recorded.
          example: +1-555-0123
        timezone:
          anyOf:
            - type: string
            - type: "null"
          description: Patient IANA timezone (e.g. America/New_York), used by the appointments surface for slot offsets. Returned as stored, which may include legacy non-IANA values. May be null when not recorded.
          example: America/New_York
        created_at:
          type: string
          description: ISO-8601 timestamp at which this patient was added to the partner roster.
          example: 2026-05-28T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp at which this patient record was last updated.
          example: 2026-05-28T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp at which this patient was removed from the partner roster, or null when active. Only returned when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
        linked_partner_practitioner_ids:
          type: array
          items:
            type: string
            format: uuid
            pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: IDs of the practitioners currently linked to this patient. Treat as an unordered set; an empty array means no current links. Present only when `include_linked_practitioners=true`.
          example:
            - b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
            - a6e7d5c3-2b3c-4d5e-9f0a-1b2c3d4e5f6a
      required:
        - id
        - partner_clinic_id
        - created_by_partner_practitioner_id
        - first_name
        - last_name
        - preferred_name
        - date_of_birth
        - email
        - phone_number
        - timezone
        - created_at
        - updated_at
        - linked_partner_practitioner_ids
      additionalProperties: false
      title: PartnerPatientWithLinkedPractitioners
      description: A patient record with the optional `linked_partner_practitioner_ids` field. Returned by GET /v1/partner/patients/{id} when `include_linked_practitioners=true`.
    PartnerPatientUpdateResponse:
      type: object
      properties:
        data:
          description: The updated patient record. Does not include `linked_partner_practitioner_ids`.
          $ref: "#/components/schemas/PartnerPatient"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPatientUpdateResponse
      description: Envelope for PATCH /v1/partner/patients/{id} (200). `data` is the updated patient; `meta` carries the request id and generation time.
    PartnerPlayroomListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPlayroom"
          description: Page of playrooms.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPlayroomListResponse
      description: Response envelope for `GET /v1/partner/playrooms`. `data` is the page of rooms; `meta` carries the request id, timestamp, and cursor pagination state.
    PartnerPlayroom:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace playroom id. Use it to preselect this room when starting a session, or to address the room on `GET/{id}` and `PATCH`.
          example: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
        title:
          type: string
          description: Clinician-supplied room name, shown in the room picker.
          example: Calm Down Corner
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Optional longer note about when to use the room, or null.
          example: For the first ten minutes of a session with younger clients.
        room_type:
          $ref: "#/components/schemas/PartnerPlayroomRoomType"
        color_palette:
          $ref: "#/components/schemas/PartnerPlayroomColorPalette"
        session_items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
          description: "Items enabled inside the room. An empty array is legitimate: it produces a room with no activities."
          example:
            - activity_shelf
            - whiteboard
            - sand_tray
        tags:
          type: array
          items:
            type: string
          description: Free-text labels the clinician filters their own rooms by.
          example:
            - anxiety
            - intake
        published:
          type: boolean
          description: "Whether the room is offered when starting a session. An unpublished room is a draft: it stays in the library but cannot be picked."
          example: true
        is_default:
          type: boolean
          description: True for the starter rooms PlaySpace creates with every new practitioner account. Read-only — you cannot create or clear a default room.
          example: false
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Preview image for the room, derived from `room_type` and `color_palette`. Stable and safe to cache; relative paths resolve against the PlaySpace app origin.
          example: /assets/playroom/thumbnails/Child_1.avif
        created_at:
          type: string
          description: ISO-8601 timestamp at which the room was created.
          example: 2026-08-18T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp at which the room was last updated.
          example: 2026-08-18T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp at which the room was removed, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - title
        - description
        - room_type
        - color_palette
        - session_items
        - tags
        - published
        - is_default
        - thumbnail_url
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerPlayroom
      description: One playroom owned by the acting practitioner.
      example:
        id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
        title: Calm Down Corner
        description: For the first ten minutes of a session with younger clients.
        room_type: child
        color_palette: color1
        session_items:
          - activity_shelf
          - whiteboard
          - sand_tray
        tags:
          - anxiety
        published: true
        is_default: false
        thumbnail_url: /assets/playroom/thumbnails/Child_1.avif
        created_at: 2026-08-18T15:30:00.000Z
        updated_at: 2026-08-18T15:30:00.000Z
    PartnerPlayroomResponse:
      type: object
      properties:
        data:
          description: The playroom at the requested id.
          $ref: "#/components/schemas/PartnerPlayroom"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPlayroomResponse
      description: Response envelope for a single playroom. `data` is one room; `meta` carries the request id and timestamp, with no pagination.
    PartnerContentListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerContentItem"
          description: Everything attached to this space, ordered by `order_index` where the combination has one and by attachment time otherwise. Empty when the space has no content on its shelf.
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerContentListResponse
      description: Response envelope for the content of one playroom or toolkit. The whole shelf is returned in one response — there is no pagination.
    PartnerContentItem:
      type: object
      properties:
        content_type:
          $ref: "#/components/schemas/PartnerContentType"
        content_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Id of the attached worksheet, storybook or form — the same id `GET /v1/partner/worksheets`, `GET /v1/partner/storybooks` and `GET /v1/partner/forms` return, and the id to use when attaching or detaching.
          example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
        title:
          type: string
          description: Display title of the attached content, as it appears on the shelf. Clinician-authored or model-generated free text — treat it as patient-adjacent and keep it out of logs and analytics.
          example: Feelings Thermometer
        order_index:
          description: Position on the shelf, lowest first, or null when the item was attached without one. The key is ABSENT for a worksheet on a playroom — the one combination of the six PlaySpace does not order, because that shelf has no ordering to store; those worksheets are returned in attachment order. It is present, though possibly null, for a storybook or a form on a playroom and for every content type on a toolkit.
          example: 0
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
        created_at:
          type: string
          description: ISO-8601 timestamp at which the content was attached to this space. This is the attachment time, not the time the worksheet, storybook or form itself was created.
          example: 2026-08-18T15:30:00.000Z
      required:
        - content_type
        - content_id
        - title
        - created_at
      additionalProperties: false
      title: PartnerContentItem
      description: One worksheet, storybook or form attached to a playroom or a toolkit. Identifies the content, not a copy of it — read the content itself through its own endpoint.
      example:
        content_type: worksheet
        content_id: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
        title: Feelings Thermometer
        created_at: 2026-08-18T15:30:00.000Z
    PartnerContentItemResponse:
      type: object
      properties:
        data:
          description: The one attachment addressed by the request path.
          $ref: "#/components/schemas/PartnerContentItem"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerContentItemResponse
      description: Response envelope for a single attachment. `data` is one shelf item; `meta` carries the request id and timestamp, with no pagination.
    PartnerContentAttachProblem:
      type: object
      properties:
        type:
          type: string
          format: uri
          description: Absolute URI identifying the problem type. All partner-API problem types live under https://api.playspace.health/problems/<slug>. The URI is a stable identifier and is not guaranteed to resolve.
          example: https://api.playspace.health/problems/unauthorized
        title:
          type: string
          minLength: 1
          description: Short, human-readable summary of the problem type.
          example: Unauthorized
        status:
          type: integer
          minimum: 400
          maximum: 599
          description: HTTP status code for this occurrence.
          example: 401
        detail:
          description: Human-readable explanation specific to this occurrence.
          type: string
        instance:
          description: URI reference identifying the specific occurrence — typically the request path.
          type: string
        reason:
          description: Present only on the shelf-readiness failure. `not_shelf_ready` means the content exists and is yours, but its publication state is not the one the in-session shelf renders, so attaching it would create a shelf entry no session participant can see. Publish the content and retry; `shelf_ready` on the content list responses tells you in advance which ids an attach will accept.
          example: not_shelf_ready
          type: string
          enum:
            - not_shelf_ready
      required:
        - type
        - title
        - status
      additionalProperties: {}
      title: PartnerContentAttachProblem
      description: Problem-details body for the 422 returned when attaching content. Extends the base Problem object with an optional `reason` discriminator, which carries `not_shelf_ready` when the attach was refused because the content is not in a shelf-renderable published state.
    PartnerPractitionerListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerPractitioner"
          description: A page of practitioner records.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPractitionerListResponse
      description: A page of practitioners with pagination metadata.
    PartnerPractitioner:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Stable identifier for this practitioner. Use it on every subsequent practitioner request.
          example: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_clinic_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: Identifier of the clinic this practitioner belongs to.
          example: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        role:
          type: string
          enum:
            - member
            - admin
            - owner
          description: "The practitioner's membership role in this clinic: `member` (default), `admin`, or `owner`. Informational metadata in this version; it does not affect API authorization."
          example: member
        status:
          type: string
          enum:
            - active
            - disabled
          description: "Whether per-clinician (delegated) access for this practitioner is currently allowed: `active` or `disabled`. While disabled, delegated tokens for this practitioner are rejected with a 403 `practitioner-disabled` problem. Disabling is reversible via PATCH and keeps all records; organization tokens are unaffected."
          example: active
        playspace_account_linked_at:
          anyOf:
            - type: string
            - type: "null"
          description: ISO-8601 timestamp when this practitioner also gained a direct PlaySpace login through the clinic-upgrade flow, or `null`. Read-only. Your integration's access to the practitioner is unchanged by it.
          example: null
        playspace_login_status:
          type: string
          enum:
            - not_invited
            - invited
            - linked
            - skipped
            - expired
          description: "Where this practitioner stands with a direct PlaySpace login after a clinic upgrade. `not_invited`: no link has been sent. `invited`: a set-password link is live. `linked`: they sign in directly (same signal as `playspace_account_linked_at`). `skipped`: the link could not be sent, or the email already had a PlaySpace login. `expired`: the last link lapsed unused. Read-only; your integration's access is unchanged by it."
          example: not_invited
        first_name:
          type: string
          description: Practitioner given name.
          example: Test
        last_name:
          type: string
          description: Practitioner family name.
          example: Practitioner
        email:
          type: string
          format: email
          pattern: ^(?!\.)(?!.*\.\.)([A-Za-z0-9_'+\-\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\-]*\.)+[A-Za-z]{2,}$
          description: Practitioner work email address, also used as their login identity.
          example: practitioner-1@clinic.example
        country:
          anyOf:
            - type: string
            - type: "null"
          description: The country this practitioner works in, as an uppercase ISO 3166-1 alpha-2 code, or `null` when it has not been set. Some PlaySpace features are only offered in certain countries, so a practitioner with no country set is treated as outside every country-restricted feature.
          example: US
        created_at:
          type: string
          description: ISO-8601 timestamp when the practitioner record was created.
          example: 2026-05-22T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp when the practitioner record was last updated.
          example: 2026-05-22T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp when the practitioner was removed, or `null` when active. Only present when the request sets `include_deleted=true`.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - partner_clinic_id
        - role
        - status
        - playspace_account_linked_at
        - playspace_login_status
        - first_name
        - last_name
        - email
        - country
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerPractitioner
      description: A practitioner record, bound to a single clinic.
      example:
        id: b7f8e6a4-1c2d-4e5f-9a0b-1c2d3e4f5a6b
        partner_clinic_id: c8e7d6b3-2a3b-4c5d-8e9f-0a1b2c3d4e5f
        role: member
        status: active
        playspace_account_linked_at: null
        playspace_login_status: not_invited
        first_name: Test
        last_name: Practitioner
        email: practitioner-1@clinic.example
        country: US
        created_at: 2026-05-22T15:30:00.000Z
        updated_at: 2026-05-22T15:30:00.000Z
    PartnerPractitionerResponse:
      type: object
      properties:
        data:
          description: The requested practitioner record.
          $ref: "#/components/schemas/PartnerPractitioner"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerPractitionerResponse
      description: A single practitioner record.
    PartnerSessionConfigResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerSessionConfig"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerSessionConfigResponse
      description: Top-level envelope for the session-configuration endpoints.
    PartnerSessionConfig:
      type: object
      properties:
        supplied_video:
          type: object
          properties:
            enabled:
              type: boolean
            approved_origins:
              type: array
              items:
                type: string
            revision:
              type: integer
              minimum: 0
              maximum: 9007199254740991
          required:
            - enabled
            - approved_origins
            - revision
          additionalProperties: false
          description: PlaySpace-managed supplied video policy. Read-only regardless of partner_editable_fields. When enabled, every new virtual appointment requires its own clinician and patient video URLs from an approved HTTPS origin. Existing appointments retain their video ownership.
        video_provider:
          type: string
          enum:
            - Whereby
            - Dailyco
            - EightxEight
            - None
          description: "Which video vendor PlaySpace opens sessions with. 'None' means your organization runs video elsewhere: PlaySpace will not create a meeting for your appointments and will not return join links for them."
          example: None
        enabled_features:
          type: object
          propertyNames:
            type: string
          additionalProperties:
            type: boolean
          description: The feature switches this organization has set. A feature not listed here has no setting and behaves as normal.
          example:
            clinical_notes: false
        allowed_content:
          anyOf:
            - type: object
              properties:
                toolkit_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                playroom_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                worksheet_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
                game_ids:
                  type: array
                  items:
                    type: string
                    format: uuid
                    pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$
              additionalProperties: false
              description: Optional narrowing of the default session catalog to the listed items. Every id must be a UUID; a malformed id is refused rather than dropped, because a partially applied allowlist would curate the catalog in a way you did not ask for. Omit a key to leave that part of the catalog whole.
              example:
                game_ids: []
            - type: "null"
          description: The session catalog narrowing, or null when the whole default catalog is offered.
          example: null
        branding:
          anyOf:
            - type: object
              propertyNames:
                type: string
              additionalProperties: {}
            - type: "null"
          description: White-label branding, or null when none is set.
          example: null
        waiting_room_enabled:
          type: boolean
          description: Whether a patient waits for the clinician to admit them before the session opens.
          example: true
        default_camera_on:
          type: boolean
          description: Whether cameras start on when a session opens.
          example: true
        recording_enabled:
          type: boolean
          description: Whether sessions may be recorded.
          example: false
        upgrade_enabled:
          type: boolean
          description: Whether your framed PlaySpace workspace offers the "Upgrade to PlaySpace" page to eligible clinic owners (the owner of a clinic, or its only practitioner, while the clinic has not finished upgrading). Off unless you turn it on.
          example: false
        partner_editable_fields:
          type: array
          items:
            type: string
          description: The fields your organization may change through this API. Read-only, and empty by default — ask PlaySpace to open the ones you need. A PATCH of any field not listed here is refused with 403 and changes nothing.
          example:
            - enabled_features
            - video_provider
        is_active:
          type: boolean
          description: Whether the PlaySpace session surface is switched on for your organization. Read-only.
          example: true
        updated_at:
          anyOf:
            - type: string
            - type: "null"
          description: ISO-8601 timestamp of the last change, or null when the configuration has never been changed.
          example: 2026-09-03T05:10:46.000Z
      required:
        - supplied_video
        - video_provider
        - enabled_features
        - allowed_content
        - branding
        - waiting_room_enabled
        - default_camera_on
        - recording_enabled
        - upgrade_enabled
        - partner_editable_fields
        - is_active
        - updated_at
      additionalProperties: false
      title: PartnerSessionConfig
      description: Your organization's session and workspace configuration, as it stands now.
    PartnerStorybookListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerStorybook"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerStorybookListResponse
      description: Top-level envelope for GET /v1/partner/storybooks. meta.pagination carries the cursor block.
    PartnerStorybook:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace storybook id. Use it to open the book in an embedded reader.
        title:
          type: string
          description: Model-generated title. May echo details from the clinician prompt — treat as patient-adjacent content.
          example: Finn the Brave Fox Asks for Help
        status:
          type: string
          description: Generation state. `ready` books are readable; `generating` ones are still being illustrated; `failed` ones will not complete.
          example: ready
        page_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of pages currently in the book.
          example: 4
        shelf_ready:
          type: boolean
          description: "Read-only and derived: true when this storybook is ELIGIBLE to be placed on a playroom or toolkit shelf — that is, when an attach will accept this id. Eligibility is not placement: an eligible book may be on no shelf at all, and a book already on one stops being eligible if its publication state changes. Attaching an id whose `shelf_ready` is false is refused with a 422 carrying `reason: not_shelf_ready`, so read this before attaching rather than after."
          example: true
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null when none has been rendered yet. Expires — re-list rather than caching it.
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        deleted_at:
          description: ISO-8601 timestamp at which the storybook was deleted, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - title
        - status
        - page_count
        - shelf_ready
        - thumbnail_url
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerStorybook
      description: One storybook owned by the acting practitioner.
    PartnerStorybookDetailResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerStorybookDetail"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerStorybookDetailResponse
      description: Top-level envelope for GET /v1/partner/storybooks/{id}.
    PartnerStorybookPagesResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerStorybookPage"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerStorybookPagesResponse
      description: Top-level envelope for GET /v1/partner/storybooks/{id}/pages. Every page of the book, in reading order — a book is small enough that this is never paginated.
    PartnerStorybookPageResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerStorybookPage"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerStorybookPageResponse
      description: Top-level envelope for GET /v1/partner/storybooks/{id}/pages/{pageId}.
    PartnerStorybookShareResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerStorybookShare"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerStorybookShareResponse
      description: Top-level envelope for POST /v1/partner/storybooks/{id}/share.
    PartnerStorybookShare:
      type: object
      properties:
        share_id:
          anyOf:
            - type: string
            - type: "null"
          description: Handle for this share, or null if one could not be recorded. The email is sent either way.
        storybook_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The storybook that was sent.
        patient_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The patient it was sent to.
        expires_at:
          type: string
          description: ISO-8601 instant after which the emailed link stops working.
          example: 2026-09-15T15:30:00.000Z
      required:
        - share_id
        - storybook_id
        - patient_id
        - expires_at
      additionalProperties: false
      title: PartnerStorybookShare
      description: Result of emailing a storybook to a patient. Carries no address and no link — both are known only to the recipient.
    PartnerToolkitListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerToolkit"
          description: Page of toolkits.
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerToolkitListResponse
      description: Response envelope for `GET /v1/partner/toolkits`. `data` is the page of toolkits; `meta` carries the request id, timestamp, and cursor pagination state.
    PartnerToolkit:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace toolkit id. Use it to preselect this toolkit when starting a session, or to address it on `GET/{id}` and `PATCH`.
          example: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
        title:
          type: string
          description: Clinician-supplied toolkit name, shown in the picker.
          example: Anxiety Toolkit
        description:
          anyOf:
            - type: string
            - type: "null"
          description: Optional longer note about when to use the toolkit, or null.
          example: Grounding activities for the middle of a session.
        session_items:
          type: array
          items:
            $ref: "#/components/schemas/PartnerSessionItem"
          description: "Items enabled in the toolkit. An empty array is legitimate: it produces a toolkit with no activities."
          example:
            - activity_shelf
            - whiteboard
        tags:
          type: array
          items:
            type: string
          description: Free-text labels the clinician filters their own toolkits by.
          example:
            - anxiety
        published:
          type: boolean
          description: "Whether the toolkit is offered when starting a session. An unpublished toolkit is a draft: it stays in the library but cannot be picked."
          example: true
        is_default:
          type: boolean
          description: True for the starter toolkits PlaySpace creates with every new practitioner account. Read-only — you cannot create or clear a default toolkit.
          example: false
        created_at:
          type: string
          description: ISO-8601 timestamp at which the toolkit was created.
          example: 2026-08-18T15:30:00.000Z
        updated_at:
          type: string
          description: ISO-8601 timestamp at which the toolkit was last updated.
          example: 2026-08-18T15:30:00.000Z
        deleted_at:
          description: ISO-8601 timestamp at which the toolkit was removed, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - title
        - description
        - session_items
        - tags
        - published
        - is_default
        - created_at
        - updated_at
      additionalProperties: false
      title: PartnerToolkit
      description: One toolkit owned by the acting practitioner.
      example:
        id: d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70
        title: Anxiety Toolkit
        description: Grounding activities for the middle of a session.
        session_items:
          - activity_shelf
          - whiteboard
        tags:
          - anxiety
        published: true
        is_default: false
        created_at: 2026-08-18T15:30:00.000Z
        updated_at: 2026-08-18T15:30:00.000Z
    PartnerToolkitResponse:
      type: object
      properties:
        data:
          description: The toolkit at the requested id.
          $ref: "#/components/schemas/PartnerToolkit"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerToolkitResponse
      description: Response envelope for a single toolkit. `data` is one toolkit; `meta` carries the request id and timestamp, with no pagination.
    PartnerWorksheetListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerWorksheet"
        meta:
          $ref: "#/components/schemas/EnvelopeMeta"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerWorksheetListResponse
      description: Top-level envelope for GET /v1/partner/worksheets. meta.pagination carries the cursor block.
    PartnerWorksheet:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: PlaySpace worksheet id. Use it to open the worksheet in an embedded viewer.
        title:
          type: string
          description: The worksheet's title as it appears in the clinician's library.
          example: Feelings Thermometer
        page_count:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Number of pages in the worksheet.
          example: 3
        shelf_ready:
          type: boolean
          description: "Read-only and derived: true when this worksheet is ELIGIBLE to be placed on a playroom or toolkit shelf — that is, when an attach will accept this id. Eligibility is not placement: an eligible worksheet may be on no shelf at all, and a worksheet already on one stops being eligible if its publication state changes. Attaching an id whose `shelf_ready` is false is refused with a 422 carrying `reason: not_shelf_ready`, so read this before attaching rather than after."
          example: true
        thumbnail_url:
          anyOf:
            - type: string
            - type: "null"
          description: Short-lived signed URL for the cover image, or null when none exists. Expires — re-list rather than caching it.
        created_at:
          type: string
          description: ISO-8601 creation timestamp.
        updated_at:
          type: string
          description: ISO-8601 last-update timestamp.
        description:
          anyOf:
            - type: string
            - type: "null"
          description: The worksheet's description, or null when it has none. Clinician-authored library text.
          example: Scaling exercise used at the start of a session.
        category:
          anyOf:
            - type: string
            - type: "null"
          description: Free-text category the worksheet is filed under, or null when it has none.
          example: Emotional regulation
        tags:
          type: array
          items:
            type: string
          description: Tags on the worksheet. Empty when it has none — never null.
          example:
            - anxiety
            - scaling
        deleted_at:
          description: ISO-8601 timestamp at which the worksheet was deleted, or null when active. Returned only when `include_deleted=true`; otherwise omitted.
          example: null
          anyOf:
            - type: string
            - type: "null"
      required:
        - id
        - title
        - page_count
        - shelf_ready
        - thumbnail_url
        - created_at
        - updated_at
        - description
        - category
        - tags
      additionalProperties: false
      title: PartnerWorksheet
      description: One worksheet in the acting practitioner's library.
    PartnerWorksheetDetailResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerWorksheetDetail"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerWorksheetDetailResponse
      description: Top-level envelope for GET /v1/partner/worksheets/{id}.
    PartnerWorksheetPagesResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PartnerWorksheetPage"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerWorksheetPagesResponse
      description: Top-level envelope for GET /v1/partner/worksheets/{id}/pages. Every page of the worksheet, in reading order — a worksheet is small enough that this is never paginated.
    PartnerWorksheetPageResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerWorksheetPage"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerWorksheetPageResponse
      description: Top-level envelope for GET /v1/partner/worksheets/{id}/pages/{pageId}.
    PartnerWorksheetShareResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/PartnerWorksheetShare"
        meta:
          $ref: "#/components/schemas/EnvelopeMetaWithoutPagination"
      required:
        - data
        - meta
      additionalProperties: false
      title: PartnerWorksheetShareResponse
      description: Top-level envelope for POST /v1/partner/worksheets/{id}/share.
    PartnerWorksheetShare:
      type: object
      properties:
        share_id:
          anyOf:
            - type: string
            - type: "null"
          description: Handle for this share, or null if one could not be recorded. The email is sent either way.
        worksheet_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The worksheet that was sent.
        patient_id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          description: The patient it was sent to.
        expires_at:
          type: string
          description: ISO-8601 instant after which the emailed link stops working.
          example: 2026-09-15T15:30:00.000Z
      required:
        - share_id
        - worksheet_id
        - patient_id
        - expires_at
      additionalProperties: false
      title: PartnerWorksheetShare
      description: Result of emailing a worksheet to a patient. Carries no address and no link — both are known only to the recipient.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |-
        Paste an access token you have already minted. This is the way to exercise the API from a rendered console: the token exchange itself cannot run in a browser (see `oauth2`), so obtain the token from a terminal and bring it here.

        Organization token — reads every clinic, practitioner, patient and appointment your organization owns:

        ```
        curl --request POST --url https://YOUR_TENANT.auth0.com/oauth/token \
          --header 'content-type: application/json' \
          --data '{"client_id":"...","client_secret":"...",
                   "audience":"https://playspace-ehr-api",
                   "grant_type":"client_credentials"}'
        ```

        Delegated token — additionally reaches the clinician-owned content surfaces (worksheets, storybooks, playrooms, toolkits, forms, game saves, embed tokens, game sessions), which refuse an organization token with 403. Add one field:

        ```
                   "practitioner_id":"<id from GET /v1/partner/practitioners>"
        ```

        Do not request scopes. Your grant is fixed on the credential at approval and travels inside the token; asking for one the credential lacks fails the exchange with `403 access_denied`.
    oauth2:
      type: oauth2
      description: |-
        OAuth2 client-credentials access token issued by Auth0 for your partner organization. Exchange your client id and secret for a token, then send it as `Authorization: Bearer <token>`.

        Your granted scopes are fixed on the credential at approval and are carried inside the token — you never request them. The vocabulary is `practitioners:read` (read practitioners), `practitioners:write` (create and update practitioners), `practitioners:delete` (delete practitioners), `clinics:read` (read clinics), `clinics:write` (create and update clinics), `clinics:delete` (delete clinics), `patients:read` (read patients), `patients:write` (create and update patients), `patients:delete` (delete patients), `appointments:read` (read appointments), `appointments:write` (create and update appointments), `appointments:delete` (delete appointments).

        This exchange yields an ORGANIZATION token, which reads every clinic, practitioner, patient and appointment your organization owns. The content surfaces (worksheets, storybooks, playrooms, toolkits, forms, game saves, embed tokens, game sessions) are owned by a clinician rather than by an organization and refuse an organization token with 403. For those, add `practitioner_id` to the token request to obtain a delegated token acting as that clinician.
      flows:
        clientCredentials:
          tokenUrl: https://dev-hcp1velit44csg3n.us.auth0.com/oauth/token
          refreshUrl: https://dev-hcp1velit44csg3n.us.auth0.com/oauth/token
          scopes: {}
          x-scalar-security-body:
            audience: https://playspace-ehr-api
