# Submit a reading by patient refId

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

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


# Submit a reading by patient refId [#submit-a-reading-by-patient-refid]

| Method | Path |
| --- | --- |
| `POST` | `/v1/signals/api/observation` |

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

The patient is `patientRefId` in the body, else `subject.reference` (`Patient/<refId>`).

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), every patient the body names 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]

### Body [#body]

Required. Schema `V1ObservationByRefDto`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `patientRefId` | string | No | — |
| `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` | 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 your access covers only some patients and the call names none, 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 patient with that refId 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 `-`. More than 100 patients are named. A patient is named by something other than a string. From Signals: The payload failed validation, or named no patient. |
| `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' \
  --user "${OVOK_CLIENT_ID}:${OVOK_CLIENT_SECRET}" \
  --header 'Content-Type: application/json' \
  --data @body.json
```
