Backfill missed Sleepiz nights
- Sandbox: available
- Staging: available
- Production: unknown
Refills Sleepiz nights that the live push never delivered. Ovok pulls each night from the Sleepiz public API for one device and sends it through the same ingest path as live data.
Auth: Bearer token. The caller must be a project admin. Scope: The caller's project (from the token) and its sub-projects. The device must belong to one of them. The route takes no project parameter.
Behaviour
- The body is strict:
serialNumber(6 to 10 digits),fromandto(YYYY-MM-DD). Unknown fields are rejected. fromtotois inclusive,frommust not be afterto,tomust be yesterday (UTC) or earlier, and the range is at most 7 nights. Longer gaps need several calls.- The device is looked up by its serial. It must be a Sleepiz serial, must belong to your project or one of its sub-projects (403 otherwise), and must have an assigned resident (409 otherwise). Assign the resident and call again. A serial held by devices in more than one project is refused with 409 too: ingest routes its readings to none of them.
- Nights are matched to the device's patient deployments in Sleepiz. Each night in the response has an
outcome:posted,no_recording,no_deploymentorerror. One night failing does not stop the others. postedmeans queued, not stored. Ovok queues the night's Observations through the live ingest router and nothing is written to Medplum during the request.jobIdslists the queued jobs andmissingBlocksnames data Sleepiz had no samples for.- Re-running the same device and range is safe. The queue job ids are deterministic and the Observation write is conditional on its event id, so no duplicates appear.
- The route answers 503 when the Sleepiz credentials are not configured on the server, and 502 when the Sleepiz API cannot be reached.
- Last night needs no call. Every day at 11:00 UTC ovok runs the same refill for yesterday's night on every device that has a resident and no recording of that night yet, except the stakeholder beds. Use this route for older nights.
Example
curl -X POST 'https://api.sandbox.ovok.com/sleepiz/backfill' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NjcyNjg4MDAsImV4cCI6MTc2NzI3MjQwMH0.Ql3B_ztDvctlyZ2z0vp4sl3ExXRQGkzmxdmTAqxNzZk' \ -H 'Content-Type: application/json' \ -d '{ "serialNumber": "<serial>", "from": "2026-01-01", "to": "2026-01-03" }'bearerAuthorizationBearer <JWT>application/json- body
serialNumber*stringThe Sleepiz device serial, 6 to 10 digits.
^\d{6,10}$from*stringFirst night to refill, inclusive (UTC date).
^\d{4}-\d{2}-\d{2}$to*stringLast night to refill, inclusive (UTC date). Yesterday or earlier.
^\d{4}-\d{2}-\d{2}$One result per night in the range.
application/json- response
serialNumber*stringThe device serial from the request.
nights*array<>curl -X POST 'https://api.sandbox.ovok.com/sleepiz/backfill' \ -H 'Content-Type: application/json' \ -d '{ "serialNumber": "string", "from": "string", "to": "string"}'{ "serialNumber": "string", "nights": [ { "night": "string", "outcome": "posted", "missingBlocks": [ "heart_rate_batch" ], "jobIds": [ "string" ], "error": "string" } ]}Update tenant settings PATCH
`settings` is merged, so a switch left out is left alone. Takes effect on the next request — except `throttleLimit`, which each replica caches for up to a minute. `episodicAlerts` gates episode recording and the notification feed. `synchronousMode` makes ingest return before evaluating. `throttleLimit` overrides THROTTLE_LIMIT for this tenant; `0` exempts it. `informationalCodes`, `historyCategories` and `feedCodes` (lists of strings, none by default): the codes the tenant's triggers consume and never judge (no no-verdict, and the `*` trigger skips them), the reading categories stored and never judged, and the codes the notification feed lists by default (none: every code). A new tenant is given the default pack's lists, and a tenant that states no `historyCategories` keeps the default pack's. `ruleSet` is deprecated and ignored: readings are judged by the triggers in the rule settings, the tenant's (`orgSettings`) and each patient's own (`/api/patient/{id}/settings`). `orgSettings` is the tenant's rule settings, which replace `/api/settings/org`: what the triggers judge by, the triggers themselves included. It is merged key by key: a key sent replaces the stored one whole, `null` removes it, and a key left out is kept. Its first write is given the default pack when it sends no `triggers`. Audited as `UPDATE_ORG_SETTINGS`. A band it changes ends the episodes judged under the old one, and a code it switches off (its band `"NO_THRESHOLD"`, or out of bed `enabled: false`) has its active alerts resolved and its open episodes closed `THRESHOLD_CHANGED` at the save, for every patient judged by the tenant's settings (an acknowledged out-of-bed absence, whose episode the ack closed, has its still-active alert resolved); a band deleted closes nothing. A tenant whose only org settings are one project's, with no patient stamped with another project, has them made the tenant's first, so the write reaches the patients they served. `resolvedAt` (ISO 8601) is when the caller resolved `orgSettings`: it lands only over a write resolved earlier, or stored without one, and is otherwise refused 409 `STALE_SETTINGS` with nothing written. With `ifAbsent=true` each key of `orgSettings` is written only where the stored settings lack it, so a default seeded at enrolment cannot overwrite a concurrent save. ### Through ovok - **Who can call it:** Manager apps, and users whose access policy grants the capabilities. - **Capabilities, all required:** `signals_config:update`, `signals_silence:update`. Project admins have every capability; client applications get no admin bypass. - Only `orgSettings`, `resolvedAt` and `settings.episodicAlerts` are written through ovok. Turning `episodicAlerts` off stops Signals recording episodes, which empties episode feeds. - It can reach any patient of your project without naming one, so it is for callers that reach every patient: project admins, manager or submitter apps, and anyone whose Patient access has no criteria.
List change history GET
Lists the recorded changes to thresholds, locations, zones, devices, residents, and users and their roles in your project, newest first. Use it for the resident and administration change-history screens. **Auth:** bearer token. The caller must be a practitioner, an admin or a System Owner, and needs `Provenance:read`. Only the Admin and System Owner access policies carry it, so other users get no change history. **Scope:** the caller's project (from the token). The rows are read with your token, so you only see changes recorded in your project. ## Behaviour - Each row is one recorded change. `details.key` names the change and `details.values` holds the values to put into its message. - Rows are sorted by recording time, newest first. `sort=asc` returns the same rows oldest first. Rows recorded in the same millisecond keep one fixed order, so paging never skips or repeats a row. - Without `startDate`, the list starts 30 days before now. `endDate` is optional. Both bounds are inclusive. - `types` limits the rows to the given change types. Omit it to get every type. Repeat the parameter to pass several types. - `scope=global` keeps only project-wide changes: organization threshold edits, device, location and zone lifecycle changes, and user and role changes. `scope=all` (the default) returns every row. - `types=Access` lists the user and role changes: a user invited (`userInvited`), renamed (`userRenamed`), moved to another role (`userRoleChanged`) or removed (`userRemoved`), and a role created (`roleCreated`), given other permissions (`rolePermissionsChanged`) or deleted (`roleDeleted`). They name users and roles by name and concern no resident. - `userId`, `residentId`, `deviceId`, `locationId` and `zoneId` are ids, not references. Each takes several ids, repeated or comma-separated, and matches a row concerning any of them. Different filters combine with AND. `userId` matches the practitioner who made the change. A value that is not an id answers 422. - `relatedTo` takes typed references (`Patient/<id>`, `Device/<id>`, `Location/<id>`, `Organization/<id>`), repeated or comma-separated, and returns every row concerning any of them, once each. - `actions` keeps the kinds of change you name: `created`, `deleted`, `renamed`, `moved`, `assigned`, `unassigned`, `discharged`, `reactivated`, `thresholdChanged`. Rows recorded before this filter shipped carry no kind, so they never match it. - `resident` on a row is the resident it concerns, `{patientId, residentId}`, or null when it concerns none. `residentId` inside it is their Resident ID, or null when they have none: show a placeholder, never a name. The flat `residentId` carries the same Resident ID, or is empty. Neither is ever a name or an internal id. - Threshold and Signals settings changes made through the Signals API are listed as Threshold changes, with the reason given. - Changes a device makes itself, such as a baseline re-push after a new 7-day mean, are never listed, and never counted in `total`. - Unknown query parameters are ignored. - `page` is zero-based and `count` defaults to 50, maximum 100. `total` counts only rows the list can return, and never exceeds 10,000. Pages from offset 10,000 on come back empty, so narrow the filters or the date range to reach older rows. ## Example ```bash curl -X GET 'https://api.sandbox.ovok.com/v1/slim/change-history?types=Threshold&types=Resident&page=0&count=50' \ -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3NjcyNjg4MDAsImV4cCI6MTc2NzI3MjQwMH0.Ql3B_ztDvctlyZ2z0vp4sl3ExXRQGkzmxdmTAqxNzZk' ```