Submit a reading for a patient id
Submit a reading for a patient id, through Ovok's Signals gateway.
Submit a reading for a patient id
| Method | Path |
|---|---|
POST | /v1/signals/api/observation/{patientId} |
- Sandbox: available
- Staging: available
- Production: unknown
subject is ignored: the path names the patient.
The body is a FHIR R4 Observation: code, value[x] (valueQuantity, valueInteger, valueString, valueBoolean, valueDateTime, valueCodeableConcept) 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. valueSampledData is judged by its latest sample and stored whole. A valueDateTime, on the reading or a component, is judged and aggregated as epoch ms (valueNumber) and listed as sent.
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.
Request
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
patientId | string | Yes | The Signals patient id. |
Body
Required. Schema V1ObservationDto.
| Field | Type | Required | Description |
|---|---|---|---|
resourceType | string | No | — |
identifier | array of object | No | — |
identifier[].system | string | No | — |
identifier[].value | string | No | — |
refId | string | No | — |
status | string | No | — |
category | array of object | No | Its codings' codes are stored; a code the tenant names in historyCategories makes the reading history: stored, never judged. |
note | object | No | 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 | object | Yes | — |
code.coding | array of object | No | — |
code.text | string | No | — |
subject | object | No | — |
subject.reference | string | No | — |
subject.display | string | No | — |
component | array of object | No | — |
component[].code | object | Yes | — |
component[].dataAbsentReason | object | No | — |
component[].valueQuantity | object | No | — |
component[].valueSampledData | object | No | — |
component[].valueString | string | No | — |
component[].valueInteger | number | No | — |
component[].valueBoolean | boolean | No | — |
component[].valueDateTime | string | No | — |
component[].valueCodeableConcept | object | No | — |
device | object | No | id (v1) or reference (FHIR) names the device. |
dataAbsentReason | object | No | v1's string or FHIR's CodeableConcept; untyped so that neither is refused. |
observedAt | string | No | — |
effectiveDateTime | string | No | — |
effectiveInstant | string | No | — |
effectivePeriod | object | No | Only .start and .end are read, as in v1. |
issued | string | No | — |
valueQuantity | object | No | — |
valueQuantity.value | number | No | — |
valueQuantity.unit | string | No | — |
valueQuantity.system | string | No | — |
valueQuantity.code | string | No | — |
valueSampledData | object | No | — |
valueSampledData.origin | object | No | — |
valueSampledData.period | number | No | — |
valueSampledData.factor | number | No | — |
valueSampledData.lowerLimit | number | No | — |
valueSampledData.upperLimit | number | No | — |
valueSampledData.dimensions | number | No | — |
valueSampledData.data | string | No | Space-separated samples, oldest first; E, L and U are no sample. |
valueString | string | No | — |
valueInteger | number | No | — |
valueBoolean | boolean | No | — |
valueDateTime | string | No | — |
valueCodeableConcept | object | No | — |
valueCodeableConcept.coding | array of object | No | — |
valueCodeableConcept.text | string | No | — |
Responses
| Status | Meaning |
|---|---|
200 | The reading as stored, plus the ingest outcome. |
400 | A path value holds a character ovok does not send to Signals, such as a raw : or +. Percent-encode it. The query carries a raw #, or more than 200 parameters. |
401 | Your ovok access token, or your client id and secret, are missing or invalid. |
403 | ovok refused the call: you lack a capability this route needs, or your session has no project, or you can read a patient the call names but not update it. From Signals: Authenticated, but the client's role does not permit this, or it is not attached to the patient. |
404 | A patient the call names is not one you can read. From Signals: No such patient in this tenant. |
409 | Your project has no Signals tenant of its own (SIGNALS_TENANT_NOT_OWN), or it is moving to another one. From Signals: A resend of a reading already stored (DUPLICATE_READING, data.observationId); nothing was stored or evaluated. |
415 | The body is not JSON. |
422 | A patient id is not 1 to 64 letters, digits, ., _, ~ or -. From Signals: The payload failed validation. |
429 | Too many requests. Retry after the seconds in Retry-After. |
500 | ovok hit an unexpected fault, such as your project having no Signals submitter. |
502 | Signals answered with something ovok couldn't use, or refused ovok's own credential. |
503 | Signals is unavailable. Retry after the seconds in Retry-After. From Signals: The evaluation backlog is full, or the readings could not be stored and judged (no database connection free in time, a lock timeout, a stalled database). Nothing was stored; resend after Retry-After (2 s). |
504 | Signals did not answer in time. |
Example
curl --request POST 'https://api.sandbox.ovok.com/v1/signals/api/observation/:patientId' \
--user "${OVOK_CLIENT_ID}:${OVOK_CLIENT_SECRET}" \
--header 'Content-Type: application/json' \
--data @body.jsonLast updated on