Signals API - Observations

Submit a reading for a patient id

  • Sandbox: available
  • Staging: available
  • Production: unknown
POST
/v1/signals/api/observation/{patientId}

subject is ignored: the path names the patient.

The body is a FHIR R4 Observation: code, value[x] (any of FHIR R4's eleven) or dataAbsentReason, and a combined device sample's vitals as component[] (each code, value[x], optional dataAbsentReason). The clinical time is effectiveDateTime, effectiveInstant or effectivePeriod.start, else issued, else the server's receipt; device is {reference, display}; the first identifier's value is stored as v1's refId. Other FHIR elements (meta, extension, performer, interpretation, referenceRange, …) are accepted and ignored. v1's observedAt (it wins over effective[x]), refId, string dataAbsentReason and device.id still work. A node with dataAbsentReason and no value is judged as carrying no value, and the rules read the reason as absent. valueSampledData is judged by its latest sample and stored whole, and the rules read every sample as samples. A valueDateTime, on the reading or a component, is judged and aggregated as epoch ms (valueNumber) and listed as sent. A valueBoolean is judged as 1 or 0. A valueTime is judged as ms since midnight, a valuePeriod as its length in ms (none while open or without an offset), a valueRatio as its quotient, and each is also kept as text (valueString); a valueRange is judged as the text low..high.

History is never judged. A reading whose category has a coding the tenant names as history (its historyCategories; the default pack's: batch-data, statistics, presence-batch, derived) is stored and deduplicated like any other, answered with no alerts, and never judged nor read by the rules.

The caller must be one of the patient's submitters, admins included: a known patient it is not attached to is a 403, an unknown one a 404 with type PATIENT_NOT_FOUND.

Idempotent. A reading is named by its first identifier (system|value), else by its clinical time, code, device and refId. A resend of one stored within the last hour is a 409 DUPLICATE_READING whose data.observationId is the stored reading, and is neither stored nor evaluated again. A reading with neither an identifier nor a clinical time is not deduplicated.

Concurrent requests are batched server-side. With Prefer: respond-sync evaluation finishes first and the reply adds alertIds, alerts ([{id, code}]), resolvedAlertIds and ruleCoverage; without it mode is async and alerts are delivered by webhook.

Through ovok

  • Who can call it: Submitter apps, and users whose access policy grants the capabilities.
  • Capabilities, all required: signals_observations:create. Project admins have every capability; client applications get no admin bypass.
  • If your access covers only some patients (anyone but project admins, manager or submitter apps, and anyone whose Patient access has no criteria), the patient must be one you can update. A patient is named by its Signals id, or by its Medplum Patient id where Signals takes a refId; ovok asks Signals which patient a Signals id is.
  • Each reading counts its patient as monitored for billing.

Authorization

headerAuthorizationBearer <JWT>

Path Parameters

patientId*string

The Signals patient id.

Header Parameters

Prefer?string

respond-sync evaluates the readings before answering: the response reports the alerts they raised and closed, and no webhook is sent for them. Without it the readings are stored and evaluated the same way before the response, which carries no alerts: they are delivered by webhook.

Request Body

application/json
  1. body
resourceType?string
identifier?array<>
refId?string
status?string
category?array<>

Its codings' codes are stored; a code the tenant names in historyCategories makes the reading history: stored, never judged.

note?

Accepted as a plain string or as FHIR's [{text, time}] Annotations, which the mapper flattens to newline-joined text. Deliberately untyped: typing it as a string would reject real readings ovok sends today.

code*
subject?
component?array<>
device?

id (v1) or reference (FHIR) names the device.

dataAbsentReason?

v1's string or FHIR's CodeableConcept; untyped so that neither is refused.

observedAt?string
effectiveDateTime?string
effectiveInstant?string
effectivePeriod?

Only .start and .end are read, as in v1.

issued?string
valueQuantity?
valueSampledData?
valueString?string
valueInteger?number
valueBoolean?boolean
valueDateTime?string
valueTime?string
valuePeriod?
valueRange?
valueRatio?
valueCodeableConcept?

Response Body

The reading as stored, plus the ingest outcome.

application/json
  1. response
_id*string

The same value as id.

id*string
patient*string

The Signals patient id.

code*

As sent: a FHIR CodeableConcept.

valueQuantity?
observedAt?string
Formatdate-time
createdAt*string
Formatdate-time
updatedAt*string
Formatdate-time
observationIds*array<string>
mode*string

Evaluation finishes before this response in both modes. sync (only under Prefer: respond-sync) means its alerts are reported here rather than by webhook; async means no alerts are reported here and they are delivered by webhook.

Value in"sync""async"
alertIds?array<string>
alerts?array<>

Sync mode only: alertIds with each alert's code, in the same order.

resolvedAlertIds?array<string>

Sync mode only: the alerts this call closed, which no webhook reports either.

evaluated?number
ruleCoverage?
Example Request
curl -X POST 'https://api.sandbox.ovok.com/v1/signals/api/observation/string' \  -H 'Content-Type: application/json' \  -d '{  "code": {}}'
Example Response
{  "_id": "string",  "id": "string",  "patient": "string",  "code": {},  "valueQuantity": {},  "observedAt": "2019-08-24T14:15:22Z",  "createdAt": "2019-08-24T14:15:22Z",  "updatedAt": "2019-08-24T14:15:22Z",  "observationIds": [    "string"  ],  "mode": "sync",  "alertIds": [    "string"  ],  "alerts": [    {      "id": "string",      "code": "string"    }  ],  "resolvedAlertIds": [    "string"  ],  "evaluated": 0,  "ruleCoverage": {    "uncoveredCodes": [      "string"    ],    "fingerprints": {      "property1": "string",      "property2": "string"    },    "ruleSet": "string"  }}

List a patient's readings GET

Newest first by clinical time, in v1's envelope `{items, total, page, pageSize, totalPages}` with nest's `nextCursor`. `from`, `to` and `code` (comma-separated) filter as in v1; `category` (comma-separated) keeps the readings carrying any of those FHIR category codes. With `from`, a reading whose clinical time is more than an hour ahead of when it was stored is not found: the read skips the storage older than that, so it never waits on it. `pageSize` defaults to 10; above 200 it needs both `from` and `to`, and then reaches 10000. A page may start at most 10000 rows in; walk further with `cursor`. Each item is a flat timeline entry (`code`, `valueNumber`, `observedAt`, `category`, `valueSampledData`, `components`, `effectiveStart`, `effectiveEnd`, `deviceId`, …) with v1's `_id`, `id`, `patient`, `valueQuantity` and `createdAt` beside it. `fields=code,valueNumber` returns exactly those keys instead. ### Through ovok - **Who can call it:** Manager apps, and users whose access policy grants the capabilities. - **Capabilities, all required:** `signals_observations:read`. Project admins have every capability; client applications get no admin bypass. - If your access covers only some patients (anyone but project admins, manager or submitter apps, and anyone whose Patient access has no criteria), the patient must be one you can read. A patient is named by its Signals id, or by its Medplum Patient id where Signals takes a `refId`; ovok asks Signals which patient a Signals id is.

Submit several readings for one patient POST

`{observations: [<FHIR Observation>]}`, each as `POST /api/observation/:patientId` takes it. In one transaction, evaluated in the order given. Answers v1's array, one entry per reading in request order: the reading as stored, or for a resend the 409 `DUPLICATE_READING` error body in its place. An array has nowhere to carry the ingest outcome, so a caller that needs `alertIds` posts them singly. The body is a FHIR R4 Observation: `code`, `value[x]` (any of FHIR R4's eleven) or `dataAbsentReason`, and a combined device sample's vitals as `component[]` (each `code`, `value[x]`, optional `dataAbsentReason`). The clinical time is `effectiveDateTime`, `effectiveInstant` or `effectivePeriod.start`, else `issued`, else the server's receipt; `device` is `{reference, display}`; the first `identifier`'s `value` is stored as v1's `refId`. Other FHIR elements (`meta`, `extension`, `performer`, `interpretation`, `referenceRange`, …) are accepted and ignored. v1's `observedAt` (it wins over `effective[x]`), `refId`, string `dataAbsentReason` and `device.id` still work. A node with `dataAbsentReason` and no value is judged as carrying no value, and the rules read the reason as `absent`. `valueSampledData` is judged by its latest sample and stored whole, and the rules read every sample as `samples`. A `valueDateTime`, on the reading or a component, is judged and aggregated as epoch ms (`valueNumber`) and listed as sent. A `valueBoolean` is judged as 1 or 0. A `valueTime` is judged as ms since midnight, a `valuePeriod` as its length in ms (none while open or without an offset), a `valueRatio` as its quotient, and each is also kept as text (`valueString`); a `valueRange` is judged as the text `low..high`. **History is never judged.** A reading whose `category` has a coding the tenant names as history (its `historyCategories`; the default pack's: `batch-data`, `statistics`, `presence-batch`, `derived`) is stored and deduplicated like any other, answered with no alerts, and never judged nor read by the rules. The caller must be one of the patient's submitters, admins included: a known patient it is not attached to is a 403, an unknown one a 404 with type `PATIENT_NOT_FOUND`. **Idempotent.** A reading is named by its first `identifier` (`system|value`), else by its clinical time, code, device and `refId`. A resend of one stored within the last hour is a 409 `DUPLICATE_READING` whose `data.observationId` is the stored reading, and is neither stored nor evaluated again. A reading with neither an identifier nor a clinical time is not deduplicated. Concurrent requests are batched server-side. With `Prefer: respond-sync` evaluation finishes first and the reply adds `alertIds`, `alerts` (`[{id, code}]`), `resolvedAlertIds` and `ruleCoverage`; without it `mode` is `async` and alerts are delivered by webhook. ### Through ovok - **Who can call it:** Submitter apps, and users whose access policy grants the capabilities. - **Capabilities, all required:** `signals_observations:create`. Project admins have every capability; client applications get no admin bypass. - If your access covers only some patients (anyone but project admins, manager or submitter apps, and anyone whose Patient access has no criteria), the patient must be one you can update. A patient is named by its Signals id, or by its Medplum Patient id where Signals takes a `refId`; ovok asks Signals which patient a Signals id is. - Each reading counts its patient as monitored for billing.