Data Plane
Data
POST /v1/data, GET /v1/data, DELETE /v1/data — write, read, and erase structured health records.
Write structured records straight into a Subject’s store, and read them back. Every write runs through the platform’s standardization pipeline: values and units are parsed, recognized indicators get a LOINC code and a canonical name, and each record is mirrored into FHIR. Rows that don’t resolve to a code stay readable. The agent reads the same data with its built-in tools.
Health data arrives in four shapes — each has exactly one door in:
| Data shape | Typical source | How it goes in | Endpoint |
|---|---|---|---|
| Structured readings | Devices / wearables (you own the integration — write daily aggregates), manual entries | Structured records | POST /v1/data — pre-aggregate high-frequency samples; episodes (sleep, workouts) carry time + end_time. See the device-data cookbook. |
| Files / photos | Lab reports, checkup PDFs, phone photos | File upload | POST /v1/files — stores the original, extracts its text, and pulls out standardized readings automatically. POST /v1/standardize runs the same extraction synchronously — for example as a dry-run preview. |
| Narrative with readings | “headache all day, temperature was 38.2 °C”, dictated notes | Narrative text | POST /v1/standardize — the quantifiable readings are extracted; the narrative around them is dropped. |
| Purely subjective journal | “dizzy and a headache all afternoon”, mood notes | Single-turn agent call | POST /v1/responses with store: true — readings and durable memories are extracted from the entry. See the Journaling recipe. |
Mirobody stores source data, standardizes structured readings, and makes both available to AI. You decide how data is collected and prepared before it reaches the API.
Write records
Section titled “Write records”POST /v1/dataAuthorization: Bearer mb_live_*Content-Type: application/json| Field | Type | Description |
|---|---|---|
records | array | Each record {indicator, value, unit?, time?, end_time?, source?}. Max 500 records per request. end_time (optional, ISO time) closes an episode — sleep, a workout — that starts at time. measured_at / start_time are accepted as aliases for time — rows from POST /v1/standardize (which returns measured_at) can be forwarded as-is. |
user | string | Subject the records belong to (tenant-isolation key). |
retention | string | Required — there is no default. One of permanent / 1h / 2h / 6h / 1d / session (persistent is accepted as an alias of permanent). Timed tiers auto-delete when their window is up — 1d is the longest. Omitting it — or sending any value outside the enum — returns 400 (code: invalid_retention). |
session_id | string | Required when retention=session (400, code: invalid_session, otherwise). Use a globally unique, opaque id (for example a UUID; never reuse it across Subjects). Tags the records so DELETE /v1/sessions/{id} can purge them. |
curl https://api.mirobody.ai/v1/data \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user": "alice", "retention": "permanent", "records": [ {"indicator": "fasting_glucose", "value": 97, "unit": "mg/dL", "time": "2026-06-16T07:30:00Z"}, {"indicator": "fasting_glucose", "value": 92, "unit": "mg/dL", "time": "2026-06-17T07:25:00Z"} ] }'Response — standardized counts the records that resolved to a LOINC code on the way in:
{ "status": "ok", "ingested": 2, "standardized": 2, "subject": "alice" }Episodes (sleep, workouts)
Section titled “Episodes (sleep, workouts)”Episode-type records span a period rather than a point in time — put the start in time and the end in the optional end_time:
curl https://api.mirobody.ai/v1/data \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user": "alice", "retention": "permanent", "records": [ {"indicator": "sleep_duration", "value": 7.5, "unit": "h", "time": "2026-07-09T23:10:00Z", "end_time": "2026-07-10T06:40:00Z", "source": "garmin"} ] }'Writing device data at scale (daily aggregation, batching, vendors)? See the device-data cookbook.
Read records
Section titled “Read records”GET /v1/data?user=alice&indicator=fasting_glucose&limit=100Authorization: Bearer mb_live_*| Query | Description |
|---|---|
user | Subject to read from. |
indicator | Optional filter by indicator name (substring match). |
limit | 1–1000 (default 100). |
offset | Skip N records for paging (ordering is stable: newest first). has_more: true → request the next page with offset += limit. |
The response is an OpenAI-style list, newest first. value is the value exactly as written ("97"); the unit lives separately in parsed_unit, alongside the other machine-readable fields:
{ "object": "list", "data": [ { "id": 1287, "indicator": "fasting_glucose", "value": "97", "parsed_value": "97", "parsed_unit": "mg/dL", "loinc_code": "1558-6", "canonical_name": "Fasting glucose [Mass/volume] in Serum or Plasma", "fhir_resource_id": "8f3c9a1e-...", "time": "2026-06-16T07:30:00+00", "end_time": null, "source": "api", "comment": "" } ], "has_more": false, "subject": "alice"}| Field | Description |
|---|---|
data | The records (up to limit). |
id | Row id (integer) — pass it to DELETE /v1/data?id= for a row-level delete. |
value | The value as written (string, e.g. "97"); the unit lives in parsed_unit. |
parsed_value / parsed_unit | Numeric value + UCUM unit from the standardization pipeline (null when unparseable). |
loinc_code / canonical_name | Deterministic LOINC resolution + its display name (null when the name didn’t resolve). |
fhir_resource_id | Id of the record’s FHIR Observation mirror. |
end_time | End of the period for episode records (sleep, workouts); null for point-in-time readings. |
reference_low / reference_high / reference_text / abnormal | Reference range and abnormality flag, when the source (e.g. a lab report) carried them; null otherwise. |
source / comment | How the record was created: api (written via POST /v1/data), extract (from POST /v1/standardize), upload (read from a /v1/files upload), or consolidation (drawn from a stored conversation). Any origin tag you sent in a record’s own source field comes back in comment. |
has_more | true when this page hit limit — fetch the next page with offset += limit. |
subject | The Subject the records belong to (resolved from user). |
Erase records
Section titled “Erase records”DELETE /v1/data?user=alice&indicator=fasting_glucoseAuthorization: Bearer mb_live_*Permanently erases the Subject’s records — right-to-be-forgotten semantics (the data is removed, not merely hidden from reads). Three scopes, from narrowest to widest:
| Query | Scope |
|---|---|
id | Exactly one row — the integer ids GET /v1/data returns. Takes precedence over indicator. |
indicator | One indicator (substring match, mirroring the GET filter). |
| (neither) | All of the Subject’s records. |
DELETE /v1/data?user=alice&id=1287{ "status": "ok", "deleted": 1, "subject": "alice" }To erase everything about a Subject (records + files + conversations + the identity mapping) at once, use DELETE /v1/subjects/{user} — see Compliance.
Use-and-forget data
Section titled “Use-and-forget data”There is no retention: "none" — writing to the store while asking not to store is contradictory, and the value is rejected like any other non-enum value. Two clean patterns instead:
- Dry-run analysis, zero side effects —
POST /v1/standardizewithstore=false(the default): standardization results out, nothing written. - Short-lived working data — write with
retention: "1h"(auto-expires after an hour), or withretention: "session"+ asession_idyou delete when done.