Data
Structured Records
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. Device and wearable data is the main form these records take (below). 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, and each has exactly one endpoint to write it through:
| Data shape | Typical source | How it goes in | Endpoint |
|---|---|---|---|
| Structured records | Devices and wearables above all (you own the integration — write daily aggregates), plus 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"} ] }'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", "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). |
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.
Device and wearable data
Section titled “Device and wearable data”Device data is the main form structured records take. This is the pattern once your vendor integration — Terra, Junction, or a vendor API of your own — is already delivering samples into your backend.
Receive
Your vendor integration delivers samples, by webhook push or periodic pull.
Aggregate to daily (recommended)
Roll high-frequency series up to one record per day before writing. Episodes keep their own period.
Write with POST /v1/data
Up to 500 records per request, retention required — the contract above.
What to aggregate
Section titled “What to aggregate”POST /v1/data accepts any granularity, but for most products one row per meaningful
reading is enough — less write volume, and a series that stays easy to reason about.
- Daily totals (steps, calories, distance) — one record per day.
- Continuous series (heart rate, SpO₂ sampled every few minutes) — aggregate to a daily value such as resting or mean heart rate, and write min/max as their own indicators if you need them. Go finer only where your product genuinely needs it.
- Episodes (sleep, workouts) — one record per episode, carrying its period with
time+end_time.
A day of device records in one call
Section titled “A day of device records in one call”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": "steps", "value": 12450, "time": "2026-07-10T00:00:00Z", "source": "garmin"}, {"indicator": "resting_heart_rate", "value": 58, "unit": "/min", "time": "2026-07-10T00:00:00Z", "source": "garmin"} ] }'Batch a full day, or a backfill window, per Subject into one call. Tag the vendor in
source — it comes back in the comment field when you read the record.
On-device sample batches
Section titled “On-device sample batches”Some samples never pass through a vendor API at all: your app reads them from the phone’s own health store — Apple HealthKit, Android Health Connect, or a Bluetooth scale pairing straight with your app — and uploads them itself. That is the same endpoint, with two things to settle in the client:
- Batch and pre-aggregate on the device. A month of HealthKit heart-rate samples is tens of thousands of rows; roll them up to the daily values you actually query, then send at most 500 records per request.
- Send the sample’s own timestamp, not the upload time —
timeis when the reading was taken, andend_timecarries the period for sleep and workouts.
Use source to record where the samples came from ("healthkit", "health_connect",
your device model), so a reading stays traceable after the fact.
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.
See also
Section titled “See also”- How Standardization Works — what happens to a reading on write.
- Data Lifecycle — how records leave the platform.