# Submit a reading for a patient id

Canonical page: https://docs.ovok.com/signals/standalone/api/observations/submit-a-reading-for-a-patient-id

{/* Generated from the API Explorer document by scripts/generate-signals-api-pages.mjs. Edit ovok-core, not this page. */}


# Submit a reading for a patient id [#submit-a-reading-for-a-patient-id]

| Method | Path |
| --- | --- |
| `POST` | `/v1/signals/api/observation/{patientId}` |

<Availability route="POST /v1/signals/api/observation/{patientId}" />

`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 [#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 [#request]

### Path parameters [#path-parameters]

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `patientId` | string | Yes | The Signals patient id. |

### Body [#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 [#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 [#example]

```bash
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.json
```
