Skip to content
Get Started

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 shapeTypical sourceHow it goes inEndpoint
Structured readingsDevices / wearables (you own the integration — write daily aggregates), manual entriesStructured recordsPOST /v1/data — pre-aggregate high-frequency samples; episodes (sleep, workouts) carry time + end_time. See the device-data cookbook.
Files / photosLab reports, checkup PDFs, phone photosFile uploadPOST /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 notesNarrative textPOST /v1/standardize — the quantifiable readings are extracted; the narrative around them is dropped.
Purely subjective journal“dizzy and a headache all afternoon”, mood notesSingle-turn agent callPOST /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.

POST /v1/data
Authorization: Bearer mb_live_*
Content-Type: application/json
FieldTypeDescription
recordsarrayEach 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.
userstringSubject the records belong to (tenant-isolation key).
retentionstringRequired — 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_idstringRequired 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.
Terminal window
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" }

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:

Terminal window
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.

GET /v1/data?user=alice&indicator=fasting_glucose&limit=100
Authorization: Bearer mb_live_*
QueryDescription
userSubject to read from.
indicatorOptional filter by indicator name (substring match).
limit11000 (default 100).
offsetSkip 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"
}
FieldDescription
dataThe records (up to limit).
idRow id (integer) — pass it to DELETE /v1/data?id= for a row-level delete.
valueThe value as written (string, e.g. "97"); the unit lives in parsed_unit.
parsed_value / parsed_unitNumeric value + UCUM unit from the standardization pipeline (null when unparseable).
loinc_code / canonical_nameDeterministic LOINC resolution + its display name (null when the name didn’t resolve).
fhir_resource_idId of the record’s FHIR Observation mirror.
end_timeEnd of the period for episode records (sleep, workouts); null for point-in-time readings.
reference_low / reference_high / reference_text / abnormalReference range and abnormality flag, when the source (e.g. a lab report) carried them; null otherwise.
source / commentHow 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_moretrue when this page hit limit — fetch the next page with offset += limit.
subjectThe Subject the records belong to (resolved from user).
DELETE /v1/data?user=alice&indicator=fasting_glucose
Authorization: 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:

QueryScope
idExactly one row — the integer ids GET /v1/data returns. Takes precedence over indicator.
indicatorOne 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.

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 effectsPOST /v1/standardize with store=false (the default): standardization results out, nothing written.
  • Short-lived working data — write with retention: "1h" (auto-expires after an hour), or with retention: "session" + a session_id you delete when done.