# Mirobody documentation (full text) Source: https://docs.mirobody.ai --- # Introduction to Mirobody https://docs.mirobody.ai/en/api-reference What Mirobody is, the three stages it runs, and how to choose between Mirobody Cloud and self-hosting. import NameOrigin from '/snippets/name-origin.mdx'; import RunModes from '/snippets/run-modes.mdx'; ## What is Mirobody? **Mirobody** is a health data engine. It takes readings from any source — lab reports, wearables, genetic tests, imaging — resolves each one to a canonical code, and stores it as a comparable record an AI can reason over. The engine is open source, Apache 2.0 licensed. **Mirobody Cloud is its hosted form:** we operate the storage, the models and the keys, and expose the whole thing behind an OpenAI-compatible `/v1` API, so an existing OpenAI SDK reaches it by changing a base URL and a key. Standardization of structured readings is the same on both. If you would rather run it on your own infrastructure, that is the [Open Source](/en/self-host) tab. The fastest path to a working call is the [Quickstart](/en/api-reference/quickstart): create a key, write one record, ask one question. ## The three stages: Collect, Standardize, Answers The engine does three things — **① Collect → ② Standardize → ③ Answers** — and Cloud is the same three, with the operational half handled for you. One difference matters: on Cloud, **collection happens on your side.** You obtain the data (device OAuth, in-app capture, a user uploading a report) and hand it to us; we host, standardize and serve it. Structured readings go to [`POST /v1/data`](/en/api-reference/data). Lab PDFs, photos, and spreadsheets go to [`POST /v1/files`](/en/api-reference/files), which stores the original, extracts its text, and standardizes any readings it finds. [`POST /v1/standardize`](/en/api-reference/extract) runs that same standardization synchronously — for a dry-run preview, or on narrative text. Names resolve to **LOINC** deterministically (no LLM code-guessing), values normalize to **UCUM** units, every reading gets a **FHIR** mirror. `"血糖(空腹)"`, `"FBG"` and `"Glucose, fasting"` become one series. See [Standardization](/en/api-reference/standardization). Ask the [Answers API](/en/api-reference/chat) for a grounded, evidence-cited answer — or build a full agent on the [Agent API](/en/api-reference/responses): your own tools, stored conversations, and the openai-agents SDK working out of the box. Answering is the most direct use of standardized data, and the same records are what let you mine insights, generate a report, or raise an alert. ## Choose how you run Mirobody ## Two API surfaces, one engine `POST /v1/responses` (OpenAI Responses-compatible). Your function tools, `previous_response_id` / `session_id` state, `response.*` streaming. The openai-agents SDK needs only a new base URL. `POST /v1/chat/completions`. A closed, grounded completion — one question, one evidence-backed answer. Drop-in for any OpenAI SDK, and ideal as [a tool inside your own agent](/en/api-reference/use-as-a-tool). Not sure? [Choose your API](/en/api-reference/choose-your-api). ## Four shapes of data, four endpoints These are the same three sources the console's **Data** page offers, plus the journal case. | Source | Endpoint | | --- | --- | | **Files / photos** — lab reports, checkup PDFs, phone photos | [`POST /v1/files`](/en/api-reference/files) | | **Structured records** — device and wearable data above all, plus manual entries | [`POST /v1/data`](/en/api-reference/data) | | **Narrative text** — a note or a report with readings inside it | [`POST /v1/standardize`](/en/api-reference/extract) | | A purely subjective journal entry | [`POST /v1/responses`](/en/api-reference/responses) with `store: true` | Which endpoint fits which case, with worked examples, is in the [Quickstart](/en/api-reference/quickstart). ## Why Mirobody - **Grounded, not plausible** — answers can include `health_records`, `citations`, and the server tool steps used to gather evidence. - **Standardized structured data** — LOINC + UCUM + FHIR on records written through `/v1/data` or stored by `/v1/standardize`; your analytics and the agent read the same series. - **OpenAI-compatible twice over** — Chat Completions *and* Responses protocols; existing SDKs and agent frameworks just work. - **Multi-tenant by design** — one key, an isolated Subject per end-user; per-Subject [right-to-be-forgotten deletes](/en/api-reference/overview#endpoints-at-a-glance). - **Open at the core** — Cloud runs the same [open-source engine](/en/self-host), organized around the same three stages. Self-host it and ① Collect becomes ours too: device providers pull on a schedule, and the terminology layer runs offline inside your own process. See [The Engine as a Library](/en/engine). ## Start here Key → data → standardized records → your first agent, in minutes. Answers and Agent — how to choose between the two surfaces. Auth, multi-tenancy, retention, errors, every endpoint. Which cluster to use, and what is live in each. For agents and scripts, the whole site is also published as plain text: [`/llms.txt`](/llms.txt) indexes every page, and [`/llms-full.txt`](/llms-full.txt) is the full corpus in one file. --- # Quickstart https://docs.mirobody.ai/en/api-reference/quickstart Three steps: collect your data, watch it standardize, then open Answers. import BaseUrl from '/snippets/base-url.mdx'; import IngestMentalModel from '/snippets/ingest-mental-model.mdx'; Mirobody in three steps: **① Collect data → ② Auto-standardize → ③ Answers.** Everything is OpenAI-compatible — you'll need an `mb_live_*` key and ~10 minutes. **API keys, usage, and a live Playground** live in the developer console at [**platform.mirobody.ai**](https://platform.mirobody.ai/). Sign in with an email code, open **API Keys**, create one (the secret is shown **once**): ```bash export MIROBODY_API_KEY="mb_live_..." ``` Write structured readings with [`POST /v1/data`](/en/api-reference/data) (`retention` is **required**), or upload a lab report — PDF, photo, spreadsheet — with [`POST /v1/files`](/en/api-reference/files): ```bash # Structured records 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": "FBG", "value": 92, "unit": "mg/dL", "time": "2026-06-17T07:25:00Z"} ] }' # Or a document (the original is stored right away; its text is ready a moment later) curl https://api.mirobody.ai/v1/files \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -F "user=alice" -F "file=@lab_report.pdf" ``` `user` is the [tenant-isolation key](/en/api-reference/overview#multi-tenancy-the-user-field) — pass each end-user's stable id and their data never mixes. Note the two spellings above — `fasting_glucose` and `FBG`. Read the data back and both rows carry the **same LOINC code**, a canonical name, UCUM-parsed values, and a FHIR mirror id: ```bash curl "https://api.mirobody.ai/v1/data?user=alice&limit=10" \ -H "Authorization: Bearer $MIROBODY_API_KEY" ``` ```json { "object": "list", "data": [ { "indicator": "FBG", "value": "92 mg/dL", "parsed_value": "92", "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-17T07:25:00+00", "source": "api", "comment": "" } ], "has_more": false, "subject": "alice" } ``` That's the [standardization pipeline](/en/api-reference/standardization) — deterministic name→LOINC (no LLM code-guessing) + UCUM + FHIR, on every **structured-record** write. To extract and standardize readings from a document, use [`POST /v1/standardize`](/en/api-reference/extract) with the file itself or an uploaded `file_key`. A grounded answer is three lines — the agent reads the standardized series, so "glucose" finds the rows written as "FBG": ```python Answers API (3 lines) from openai import OpenAI client = OpenAI(api_key="mb_live_...", base_url="https://api.mirobody.ai/v1") print(client.chat.completions.create(model="mirobody-flash", user="alice", messages=[{"role": "user", "content": "How is my fasting glucose trending?"}] ).choices[0].message.content) ``` And a real agent — with **your own tool** — is ten. The [openai-agents SDK](https://github.com/openai/openai-agents-python) talks to the [Agent API](/en/api-reference/responses) with only a new base URL: ```python openai-agents SDK (10 lines) from agents import Agent, ModelSettings, Runner, function_tool, set_default_openai_client, set_tracing_disabled from openai import AsyncOpenAI set_default_openai_client(AsyncOpenAI( base_url="https://api.mirobody.ai/v1", api_key="mb_live_...")) set_tracing_disabled(True) @function_tool def book_appointment(date: str) -> str: """Book a clinic appointment for the end user (ISO date).""" return f"Booked: {date} 09:30, Dr. Chen" agent = Agent(name="Health assistant", model="mirobody-flash", tools=[book_appointment], model_settings=ModelSettings(extra_body={"user": "alice"})) print(Runner.run_sync(agent, "Check my recent glucose; book a follow-up if it's trending up.").final_output) ``` Every agents-SDK agent that touches Mirobody tools must pass the Subject via `model_settings=ModelSettings(extra_body={"user": ...})`. Without it, the run reads the **account-default Subject**, not the user you meant. Mirobody's built-in tools read alice's real data; when the model decides to book, it hands off to **your** function. See [Function calling](/en/api-reference/function-calling) for the protocol underneath. ## The console Playground The console Playground follows the same flow, in the same words: **① Collect data → ② Auto-standardize → ③ Agent or ③ Answers**. Upload a file, inspect the extracted text or standardized records, and run either API without writing code. Collect data, inspect standardization, then open Agent or Answers. Request counts for the last 30 days. ## Next steps Answers vs Agent — closed completion or full agent. Client tools, stored conversations, streaming events. OCR → extraction → LOINC → UCUM → FHIR, explained. curl / Python / Node / openai-agents, end to end. --- # Choose Your API https://docs.mirobody.ai/en/api-reference/choose-your-api Answers API vs Agent API — pick the right surface in one minute. Mirobody exposes the same grounded health agent through **two surfaces**. Both answer from the Subject's real, standardized health data, and both return the tool trace behind the answer. Both are **③ Answers** — the third of the engine's three stages (① Collect → ② Standardize → ③ Answers). The choice on this page is not *what* you get but *who drives the loop*: the Answers API closes it server-side and hands you a finished answer, while the Agent API leaves it open so your own tools take part. That is the same distinction the self-hosted engine draws between its two agents, so a decision made here carries over if you later [run it yourself](/en/engine). `POST /v1/chat/completions` — closed, one-shot, evidence-backed completions. No client tools, no state. Drop-in OpenAI swap. `POST /v1/responses` — OpenAI Responses-compatible. Multi-turn state, your own function tools, `response.*` streaming. The **Answers API** speaks the older Chat Completions protocol and stays deliberately closed — no client tools, no stored state, `chat.completion.chunk` streaming only. That's what lets it drop into an existing OpenAI integration unchanged, or [wrap as a single tool](/en/api-reference/use-as-a-tool) inside a larger agent. The **Agent API** speaks the newer Responses protocol: state between turns (`store`, `previous_response_id`, `session_id`), your own function tools with a full `function_call` handoff, `response.*` events. It is what the `openai-agents` SDK talks to out of the box — change only the `base_url`. Everything else is identical: the same server-side grounding over your [standardized data](/en/api-reference/data), the same `mirobody-flash` / `mirobody-expert` models, the same Subject-based tenancy, the same usage accounting. ## See also - [Answers API (Chat Completions)](/en/api-reference/chat) — the closed, grounded completion. - [Agent API (Responses)](/en/api-reference/responses) — the open agent loop. - [Quickstart](/en/api-reference/quickstart) — a key, a record and a first answer. --- # API Overview https://docs.mirobody.ai/en/api-reference/overview OpenAI-compatible health-data API — base URL, auth, multi-tenancy, retention, errors. import BaseUrl from '/snippets/base-url.mdx'; import AuthKey from '/snippets/auth-key.mdx'; **Mirobody Cloud is OpenAI-compatible.** Point any OpenAI SDK at the base URL below, pass an `mb_live_*` key, and call the [Answers API](/en/api-reference/chat) (`/v1/chat/completions`) or the [Agent API](/en/api-reference/responses) (`/v1/responses`) — the agent answers from each end-user's **real, standardized health data**, and returns the tool trace behind the answer. Not sure which surface? See [Choose your API](/en/api-reference/choose-your-api). The same engine also runs **self-hosted** — clone the [open-source engine](/en/self-host) and bring it up with `./deploy.sh`; the hosted API adds managed storage, keys, and billing on top. Keys, usage, and an interactive Playground live in the [developer console](https://platform.mirobody.ai/). These docs are the API reference. ## Base URL Global and China are production clusters; Japan and EU are in preparation. Pick the one that matches where the data must be processed — see [Regions](/en/api-reference/regions/overview). ## Authentication Console sign-in (email code or WeChat) is a separate session login for the console itself — not an API credential, and `/v1` keys are not JWTs. ## OpenAI compatibility Standard OpenAI fields work as-is; Mirobody extensions ride along in `extra_body` (Python SDK) or as plain top-level JSON (curl/fetch). | | Fields | | --- | --- | | **Standard** | `model`, `messages` / `input`, `stream`, `user` — plus, on the [Agent API](/en/api-reference/responses): `instructions`, `store`, `previous_response_id`, `tools`, `tool_choice`, `text.format` | | **Mirobody extensions** | `retention` (data lifetime — see below), `session_id` (session scoping; on [`/v1/responses`](/en/api-reference/responses) it binds a durable conversation), `mode` + `builtin_tools` ([backbone mode](/en/api-reference/backbone-mode)), `strict` (strict validation), `reasoning` / `reasoning_effort` (deep-thinking effort — see [Agent API](/en/api-reference/responses)) | | **Response extensions** | `reasoning_content` / `reasoning` items, `tool_steps[]` (server tool trace), top-level `health_records[]` / `citations[]`, `usage.billed_tokens` (the actual token total you're metered on) | **Sampling parameters are accepted for compatibility, but the agent runs its own generation.** `max_tokens` is accepted but **not enforced**; `temperature`, `top_p`, `stop`, and `seed` are accepted but **ignored**. On the Answers API, `tools` / `tool_choice` / `response_format` / `n>1` are **explicitly rejected with `400`** — see [Unsupported parameters](/en/api-reference/chat#unsupported-parameters). ## Multi-tenancy: the `user` field Every request carries a `user` string — the **tenant-isolation key**. The backend maps `(your account, user)` to an internal **Subject**, and Subjects are fully isolated — pass each end-user's stable id as `user` and their data never crosses over. Omit it and the call falls back to your account's default Subject. Subjects are not web-app accounts — they're invisible to the Mirobody app and to other developers. ## Data retention Anything you write to the data plane (structured records, uploaded files, stored extractions) carries a `retention` that decides how long it's kept: | `retention` | Meaning | Lifetime | | --- | --- | --- | | `permanent` (alias `persistent`) | Kept until explicitly deleted | Until `DELETE /v1/data` / `DELETE /v1/files/{key}` / `DELETE /v1/subjects/{user}` | | `session` | Bound to a `session_id` | Until `DELETE /v1/sessions/{id}` | | `1d` / `6h` / `2h` / `1h` | Auto-expires after the grain | Hidden from reads at expiry, then permanently deleted (FHIR mirrors included) | On **[`POST /v1/data`](/en/api-reference/data) `retention` is required** — no default; omitting it (or any value outside the enum) returns `400` (`code: invalid_retention`). `retention=session` additionally requires a `session_id`. On [`POST /v1/standardize`](/en/api-reference/extract) it's required when `store=true`; on [`POST /v1/files`](/en/api-reference/files) it's optional (defaults to `permanent`). There is **no `retention: "none"`**. For use-and-forget analysis, run [`POST /v1/standardize`](/en/api-reference/extract) with `store=false` (dry-run — nothing persisted), or write with `retention: "1h"`. The agent **only ever reads the Subject's currently-unexpired data**. Conversation persistence on the Agent API is a separate knob (`store`) — see [State & memory](/en/api-reference/state-and-memory). For purely subjective entries (journaling), ingest them as stored single-turn agent calls — see the [Journaling recipe](/en/api-reference/state-and-memory#journaling). ## Evidence: health_records & citations Answers are **explainable and checkable** — not "sounds plausible," but "this conclusion came from that record of yours." Both API surfaces return two top-level evidence arrays of `{tool, data}` pairs: - **`health_records`** — outputs of the health-data tools the answer relied on (the Subject's actual records). - **`citations`** — outputs of the external-evidence tools (`search_medical_evidence`, `read_source`); empty when no external evidence was consulted. The full server tool trace (every call with arguments and results) is in the `tool_steps` extension. ## Error format Errors use the OpenAI-style envelope: ```json { "error": { "message": "`retention` is required.", "type": "invalid_request_error", "code": "invalid_retention", "param": "retention" } } ``` `type` is consistently `invalid_request_error` for caller mistakes; `code` and `param` identify the specific problem. Observed (status, code) pairs: | HTTP | `code` | When | | --- | --- | --- | | `400` | `invalid_retention`, `invalid_session`, … | Malformed request / missing required field — `param` names the field | | `400` | `unsupported_parameter` | A parameter this surface rejects (e.g. `tools` on the Answers API, `text.format` on agent mode, an unknown top-level param under `strict`) — `param` names it | | `401` | `null` | Missing or malformed `Authorization` header | | `401` | `invalid_api_key` | Bad or revoked `mb_live_*` key | | `404` | — | Unknown resource (`file_key` / `response_id` / `previous_response_id` / subject) | | `413` | — | Upload exceeds the size limit | | `422` | — | File text extraction failed (`/v1/standardize`) | | `429` | `rate_limit_exceeded` | Per-key request rate limit exceeded — carries `Retry-After` + `X-RateLimit-*`. See [Rate Limits & Quota](/en/api-reference/rate-limits) | | `429` | `insufficient_quota` | Monthly account usage cap reached — resets next month. See [Rate Limits & Quota](/en/api-reference/rate-limits) | | `500` | `internal_error` | Unexpected server-side failure | | `502` | — | Upstream agent error — transient; retry with backoff | In **streaming**, an upstream failure arrives as an SSE error frame (`{"error": ...}` on the Answers API, `response.failed` on the Agent API) before the stream ends. ## Endpoints at a glance | Method | Path | Purpose | | --- | --- | --- | | `GET` | [`/v1/models`](/en/api-reference/models) | List capability tiers (`mirobody-flash` / `mirobody-expert`) | | `POST` | [`/v1/chat/completions`](/en/api-reference/chat) | **Answers API** — closed grounded completion (stream, evidence) | | `POST`·`GET`·`DELETE` | [`/v1/responses`](/en/api-reference/responses) | **Agent API** — client tools, stored conversations, `response.*` streaming | | `POST` · `GET` · `DELETE` | [`/v1/data`](/en/api-reference/data) | Write / read / erase structured records (standardized on write) | | `POST` | [`/v1/standardize`](/en/api-reference/extract) | Document → standardized indicators (dry-run by default) | | `POST` · `GET` · `DELETE` | [`/v1/files`](/en/api-reference/files) | Upload & parse files (OCR / Excel), list, fetch text, delete | | `DELETE` | [`/v1/sessions/{id}`](/en/api-reference/lifecycle#sessions) | End a session and purge its session-scoped data | | `DELETE` | [`/v1/subjects/{user}`](/en/api-reference/lifecycle#offboarding-a-subject) | Erase everything for one Subject (right to be forgotten) — see [Compliance](/en/api-reference/compliance) | ## See also - [Quickstart](/en/api-reference/quickstart) — the shortest working path. - [Choose Your API](/en/api-reference/choose-your-api) — Answers or Agent. - [Rate Limits & Quota](/en/api-reference/rate-limits) — limits, quota and backoff. --- # Models https://docs.mirobody.ai/en/api-reference/models GET /v1/models — list the available capability tiers. ## Endpoint ```http GET /v1/models ``` Lists the capability tiers you can pass as `model` to the [Answers API](/en/api-reference/chat) and the [Agent API](/en/api-reference/responses). OpenAI-compatible shape. **Public** — the model catalog is a static capability list (no tenant data), so this endpoint needs **no API key**. An `Authorization` header, if sent (the OpenAI SDKs always attach one), is ignored. Every other `/v1` endpoint requires `Authorization: Bearer mb_live_*`. ## Tiers | `model` | Tier | Use case | | --- | --- | --- | | `mirobody-flash` | Default — fast, low cost | Everyday health Q&A, high concurrency | | `mirobody-expert` | Deeper reasoning | Complex report interpretation, multi-step analysis | Hosted model usage is metered from the backing provider's token counts. Each model response includes `usage.billed_tokens`; combine those counts with the current catalog's `pricing` to estimate cost. The console Usage page reports request counts, not token-level cost. The open-source engine is free software — self-host it and you pay only for your own infrastructure and LLM providers. ## Per-tier metadata Each entry carries a conservative capacity budget and the per-tier price: | Field | Meaning | | --- | --- | | `context_window` | Advertised input-plus-output capacity for the tier. Treat it as a planning budget, not a promise that every request shape will fit. | | `max_output_tokens` | Advertised output capacity — catalog metadata only; agent mode does not enforce request fields such as `max_tokens`. | | `pricing` | `{input_per_1m_tokens, output_per_1m_tokens, currency}` — standard price per **1M tokens** for estimating from `usage.billed_tokens`. Cache adjustments are not represented in this object. | ## Example ```bash curl # no API key needed curl https://api.mirobody.ai/v1/models ``` ```python Python (OpenAI SDK) from openai import OpenAI # api_key is unused by /v1/models but the SDK requires the field to be set client = OpenAI(api_key="unused", base_url="https://api.mirobody.ai/v1") for m in client.models.list().data: print(m.id) ``` ## Response An OpenAI-style `{ "object": "list", "data": [ … ] }` — each tier extends the standard model object with `context_window`, `max_output_tokens`, and `pricing`. The values below illustrate the response shape; read the live endpoint for current values: ```jsonc { "object": "list", "data": [ { "id": "mirobody-flash", "object": "model", "owned_by": "mirobody", "context_window": 65536, "max_output_tokens": 8192, "pricing": { "input_per_1m_tokens": 0.75, "output_per_1m_tokens": 4.50, "currency": "USD" } }, { "id": "mirobody-expert", "object": "model", "owned_by": "mirobody", "context_window": 65536, "max_output_tokens": 8192, "pricing": { "input_per_1m_tokens": 5.0, "output_per_1m_tokens": 25.0, "currency": "USD" } } ] } ``` Capacity, backing providers, and pricing may differ by cluster and can change without changing the stable tier id. Always read `pricing` from the cluster you call rather than hard-coding the example. Always pass a stable tier as `model` — `mirobody-flash` (default) or `mirobody-expert`. ## See also - [Rate Limits & Quota](/en/api-reference/rate-limits) — the per-key rate limit and the monthly quota. - [API Overview](/en/api-reference/overview) — authentication and the base URL. - [Regions Overview](/en/api-reference/regions/overview) — which tiers each cluster serves. --- # Files / Photos https://docs.mirobody.ai/en/api-reference/files POST /v1/files, GET /v1/files, DELETE /v1/files/{file_key} — upload reports, retrieve extracted text, list, and delete. Upload a health document to the Subject's store. Mirobody saves the original first, then in the background extracts its readable text — **OCR** for PDFs and images, a straight conversion for spreadsheets and text files — and runs the same standardization pipeline as [`POST /v1/standardize`](/en/api-reference/extract) over it, so an uploaded lab report also lands as standardized records. The agent reads both when it answers. Uploads are capped at **100 MB** per file — larger requests return `413`. For big exports or realtime streams, use the [WebSocket upload](/en/api-reference/files#websocket-upload) path. ## Upload a file ```http POST /v1/files Authorization: Bearer mb_live_* Content-Type: multipart/form-data ``` | Field | Description | | --- | --- | | `file` | The document (PDF / image / Excel / CSV). | | `user` | Subject the file belongs to. **Optional** — omitting it falls back to your account's **`default`** Subject, so always pass it for per-end-user isolation. | | `retention` | **Optional here** (unlike `POST /v1/data`, where it's required). Same values as [Data retention](/en/api-reference/overview#data-retention). | | `session_id` | Optional; scopes `retention=session` uploads to a [session](/en/api-reference/lifecycle#sessions). | ```bash curl https://api.mirobody.ai/v1/files \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -F "user=alice" \ -F "file=@checkup_2026.pdf" ``` Response: ```json { "object": "file", "id": "AUmIn_R-Ddg.../b/bM--fG29X....pdf", "filename": "checkup_2026.pdf", "bytes": 20544, "status": "processed", "created_at": 1782924296, "subject": "alice" } ``` The file's identifier is **`id`** (a slash-containing key) — use it as `{file_key}` in the fetch and delete calls below. `created_at` is **epoch seconds**. `status: "processed"` confirms the original was accepted and stored — its text may still be processing. A `GET /v1/files/{file_key}` immediately after upload can return an empty `extracted_text`; retry after a short delay. An upload makes the document's text readable by the agent, and Mirobody also pulls the report's readings out of it — [standardized](/en/api-reference/standardization) exactly like a `POST /v1/data` write — and stores them as records. Query them from [`GET /v1/data`](/en/api-reference/data) without re-entering anything; re-uploading the same file creates no duplicates. Prefer [`POST /v1/standardize`](/en/api-reference/extract) when you'd rather run that extraction yourself and check the readings first. Already have structured records? Send them to [`POST /v1/data`](/en/api-reference/data). ## List files ```http GET /v1/files?user=alice Authorization: Bearer mb_live_* ``` ```json { "object": "list", "data": [ { "object": "file", "id": "AUmIn_R-Ddg.../b/bM--fG29X....pdf", "file_key": "AUmIn_R-Ddg.../b/bM--fG29X....pdf", "filename": "checkup_2026.pdf", "file_type": "application/pdf", "bytes": 20544, "created_at": 1782924296 } ], "subject": "alice" } ``` `data` holds the files; `subject` echoes the Subject. Each item exposes both `id` and `file_key` (identical values). `created_at` is **epoch seconds**, matching the upload response. ## Fetch parsed text ```http GET /v1/files/{file_key}?user=alice Authorization: Bearer mb_live_* ``` Returns the extracted text for one file — the same content the agent reads: ```json { "object": "file", "id": "AUmIn_R-Ddg.../b/bM--fG29X....pdf", "filename": "checkup_2026.pdf", "extracted_text": "Annual checkup 2026-06-16\nFasting glucose 97 mg/dL (ref 70-110)\n...", "abstract": "", "subject": "alice" } ``` `extracted_text` can be empty while the file's text is still being processed. `abstract` may also be empty; it isn't generated for every file type. ## WebSocket upload For large exports or realtime capture, `wss://…/v1/files/stream` uploads over one socket with chunking and progress. Auth rides the query string (browsers can't set WebSocket headers): ```text wss://api.mirobody.ai/v1/files/stream?key=mb_live_...&user=alice&retention=permanent ``` Frame sequence (all JSON text frames): 1. **Server → you**: `{"type": "connection_established"}` — send nothing before this. 2. **You → server**: `{"type": "upload_start", "messageId": "", "files": [{"filename", "contentType", "size"}]}` — one `messageId` for the whole batch. 3. **You → server**, per 256 KB chunk: `{"type": "upload_chunk", "messageId", "filename", "chunk": "", "chunkIndex", "totalChunks"}`. 4. **You → server**: `{"type": "upload_end", "messageId"}` → server replies `upload_end_response` when every original file has been stored. Its text may still be processing. Same 100 MB per-file cap, same Subject isolation, and the stored files are identical to a `POST /v1/files` upload — list them with `GET /v1/files`. ## Delete a file ```http DELETE /v1/files/{file_key}?user=alice Authorization: Bearer mb_live_* ``` Removes the file from the API surface immediately (`404` if it isn't the Subject's file or is already deleted): ```json { "object": "file", "id": "AUmIn_R-Ddg.../b/bM--fG29X....pdf", "deleted": true, "subject": "alice" } ``` Deleting a file removes the **file** but **not** the records auto-extracted from it. To remove those, delete them from the data plane with [`DELETE /v1/data`](/en/api-reference/data#erase-records) (by `indicator` or `id`), or erase the whole Subject with [`DELETE /v1/subjects/{user}`](/en/api-reference/lifecycle#offboarding-a-subject). (Session-scoped uploads **are** erased together with their session-scoped records by [`DELETE /v1/sessions/{id}`](/en/api-reference/lifecycle#sessions).) Time-bounded uploads are also removed automatically at **[retention](/en/api-reference/overview#data-retention) expiry** — an expired file disappears from every `GET /v1/files*` read immediately and is then permanently deleted. To erase everything about a Subject at once, use `DELETE /v1/subjects/{user}` — see [Compliance](/en/api-reference/compliance). ## See also - [Narrative Text & Reports](/en/api-reference/extract) — standardizing a report without storing the file. - [Structured Records](/en/api-reference/data) — the structured records extraction produces. - [Data Lifecycle](/en/api-reference/lifecycle) — retention and deletion for files. --- # Structured Records https://docs.mirobody.ai/en/api-reference/data POST /v1/data, GET /v1/data, DELETE /v1/data — write, read, and erase structured health records. import SubjectUserParam from '/snippets/subject-user-param.mdx'; import IngestMentalModel from '/snippets/ingest-mental-model.mdx'; 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](/en/api-reference/data#device-and-wearable-data)). Every write runs through the platform's [standardization pipeline](/en/api-reference/standardization): 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. ## Write records ```http POST /v1/data Authorization: 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}`](/en/api-reference/lifecycle#sessions) can purge them. | ```bash 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: ```json { "status": "ok", "ingested": 2, "standardized": 2, "subject": "alice" } ``` **Every write is standardized, not just stored.** Each value and unit is parsed to UCUM, and the indicator name is matched to a LOINC code — never a guessed one, so a low-confidence match stays uncoded. See [How standardization works](/en/api-reference/standardization). To see the result without writing anything, use [`POST /v1/standardize`](/en/api-reference/extract) with `store=false`. Writes are not idempotent — a retry can create duplicate records. Check the outcome before retrying a timed-out request, and keep your own ingestion ledger. ### 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`: ```bash 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 ```http GET /v1/data?user=alice&indicator=fasting_glucose&limit=100 Authorization: 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: ```json { "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 ```http 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: | 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. | ```http DELETE /v1/data?user=alice&id=1287 ``` ```json { "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}`](/en/api-reference/lifecycle#offboarding-a-subject) — see [Compliance](/en/api-reference/compliance). ## 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. **Mirobody does not host device OAuth** — there is no Terra-style connect widget. Vendor consent screens, token refresh and webhook plumbing stay in your product, because every vendor's flow is different and it belongs in your own UX. What happens *after* the data arrives — storage, standardization, retention, deletes, AI — is Mirobody's job. Your vendor integration delivers samples, by webhook push or periodic pull. Roll high-frequency series up to one record per day before writing. Episodes keep their own period. Up to 500 records per request, `retention` required — the contract above. ### 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 ```bash 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 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 — `time` is when the reading was taken, and `end_time` carries 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 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/standardize`](/en/api-reference/extract) with `store=false` (the default): standardization results out, nothing written. - **Short-lived working data** — write with `retention: "1h"` ([auto-expires](/en/api-reference/overview#data-retention) after an hour), or with `retention: "session"` + a `session_id` you [delete](/en/api-reference/lifecycle#sessions) when done. ## See also - [How Standardization Works](/en/api-reference/standardization) — what happens to a reading on write. - [Data Lifecycle](/en/api-reference/lifecycle) — how records leave the platform. --- # Narrative Text & Reports https://docs.mirobody.ai/en/api-reference/extract POST /v1/standardize — lab report in, standardized indicators out; dry-run by default. import SubjectUserParam from '/snippets/subject-user-param.mdx'; ```http POST /v1/standardize Authorization: Bearer mb_live_* ``` `POST /v1/extract` is a deprecated alias for this endpoint and still works — same request, same response. **Document in, standardized indicators out.** `/v1/standardize` reads a lab report or health document and returns every reading it found, each matched to a LOINC code and normalized to a UCUM unit (the mechanism is in [How standardization works](/en/api-reference/standardization)). The call is **synchronous** — one request, readings back — so a full multi-page document can take several seconds; size your client timeout accordingly. By default it is a **dry-run** (`store=false`): you get the standardization result and **nothing is persisted** — zero side effects. Set `store=true` (with a `retention`) to also write those readings into the Subject's store as records, through the same pipeline as [`POST /v1/data`](/en/api-reference/data). `store=true` is not idempotent — repeating the same call can write duplicate records. Inspect a document with the default dry-run first, and don't blindly retry after a timeout. ## Request Two input shapes: - **multipart/form-data** — a `file` (PDF / image / Excel / plain text; images and PDFs are OCR'd) plus the fields below as form fields. - **application/json** — `{"text": "..."}` (raw report text) **or** `{"file_key": "..."}` (a file already uploaded via [`/v1/files`](/en/api-reference/files)). | Field | Type | Description | | --- | --- | --- | | `file` / `text` / `file_key` | — | Exactly one source. A `file_key` that is unknown or has no extractable text → `404`. | | `user` | string | The Subject this call runs for. | | `store` | bool | Default **`false`** (dry-run). `true` also ingests the readings. | | `retention` | string | **Required when `store=true`** — same enum as [`POST /v1/data`](/en/api-reference/data) (`permanent` / `1h` / `2h` / `6h` / `1d` / `session`). | | `session_id` | string | Required when `retention=session`. | ### Dry-run (default) ```bash curl https://api.mirobody.ai/v1/standardize \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -F "user=alice" \ -F "file=@lab_report.pdf" ``` ### Standardize and store ```bash curl https://api.mirobody.ai/v1/standardize \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "user": "alice", "text": "Fasting glucose 97 mg/dL (2026-07-01); LDL cholesterol 120 mg/dL", "store": true, "retention": "permanent" }' ``` ## Response ```json { "object": "extraction", "data": [ { "indicator_raw": "Fasting glucose", "canonical_name": "Fasting glucose [Mass/volume] in Serum or Plasma", "loinc_code": "1558-6", "value_raw": "97", "parsed_value": "97", "unit_raw": "mg/dL", "unit_ucum": "mg/dL", "confidence": 0.82, "measured_at": "2026-07-01" }, { "indicator_raw": "LDL cholesterol", "canonical_name": "Cholesterol in LDL [Mass/volume] in Serum or Plasma", "loinc_code": "2089-1", "value_raw": "120", "parsed_value": "120", "unit_raw": "mg/dL", "unit_ucum": "mg/dL", "confidence": 0.79, "measured_at": null } ], "stored": false, "stored_count": 0, "subject": "alice" } ``` | Field | Description | | --- | --- | | `data[]` | One row per extracted reading. | | `indicator_raw` / `value_raw` / `unit_raw` | Exactly what the document said. | | `canonical_name` / `loinc_code` | Deterministic LOINC resolution (`null` = no confident code — **never a guessed one**). | | `parsed_value` / `unit_ucum` | Parsed value (returned as a string) + UCUM-normalized unit. | | `confidence` | Similarity score of the LOINC match (0–1). | | `measured_at` | Timestamp found in the document, if any. | | `page` | 1-based source page the reading came from; present for multi-page documents. | | `abnormal` | Abnormal-range flag carried by the source (e.g. `"H"` / `"L"` / `"高"`); present when non-empty. | | `stored` | Whether this call wrote to the store (echoes `store`). | | `stored_count` | Readings actually written (`0` on a dry-run). | | `dropped` | **Optional, top-level.** Readings discarded as implausible (out-of-range or garbled values) — present when at least one was filtered out, so you can see what didn't reach `data`. | | `note` | Present **only when `data` is empty** — explains that no quantifiable readings were found (see below). | ## Narrative text with no readings `/v1/standardize` returns **quantifiable readings**. Purely narrative text — *"dizzy and a headache all afternoon"* — is a documented boundary, not an error: the call succeeds (`200`) with an empty `data` array and a `note`: ```json { "object": "extraction", "data": [], "stored": false, "stored_count": 0, "subject": "alice", "note": "no quantifiable readings found in the text; for subjective/journal entries, send a single-turn POST /v1/responses with store:true instead" } ``` Mixed text does what you'd expect — *"headache all day, temperature was 38.2 °C"* yields the temperature reading and drops the narrative. **Purely subjective entries (journaling) use the Agent API — no dedicated endpoint.** Send each entry as a single-turn [`POST /v1/responses`](/en/api-reference/responses) with `store: true`, a unique `session_id` such as `journal-{entry_id}`, `builtin_tools: "none"`, and short-acknowledgement `instructions`. Mirobody then pulls any quantifiable readings and durable memories out of the stored entry automatically. With one response per `session_id`, deleting that response also retracts the memories it produced; readings already written to the data plane stay until you delete them through `/v1/data`. See [Journaling](/en/api-reference/state-and-memory#journaling) for the exact lifecycle. ## Errors | HTTP | When | | --- | --- | | `400` | No `file` / `text` / `file_key`; `store=true` without a valid `retention`; `retention=session` without a `session_id` | | `404` | `file_key` unknown or has no extractable text | | `413` | File exceeds the upload size limit | | `422` | Text could not be extracted from the file | | `502` | Extraction model unavailable — transient; retry with backoff | Every failure is explicit — there is no silent partial success. ## See also - [How Standardization Works](/en/api-reference/standardization) — the pipeline this endpoint runs synchronously. - [Structured Records](/en/api-reference/data) — writing structured readings directly. - [Files / Photos](/en/api-reference/files) — uploading the original document instead. --- # How Standardization Works https://docs.mirobody.ai/en/api-reference/standardization How structured readings become coded, unit-normalized, FHIR-mirrored data. Health data arrives messy — "血糖(空腹)", "FBG", "Glucose, fasting" all name the same indicator, and the units vary just as much. Mirobody standardizes every **structured reading** written through [`POST /v1/data`](/en/api-reference/data) or stored by [`POST /v1/standardize`](/en/api-reference/extract), so the agent and your queries see one coherent, coded dataset. [`POST /v1/files`](/en/api-reference/files) is a separate storage and text-extraction surface; uploading a file standardizes its report values into structured readings automatically. `/v1/standardize` is the explicit path for that same extraction — use it to inspect the result (dry-run), or to standardize `text` / `file_key` sources on demand. ## The pipeline Both a document and a structured record enter the same pipeline: The important part is step 3: **codes are matched, never invented.** A model is great at *reading* a document but shouldn't be trusted to *recite* a code system — a wrong LOINC code is worse than none. So when the match isn't confident, Mirobody keeps the raw name and leaves `loinc_code` null rather than guess. When self-hosting, this pipeline covers structured readings: the engine's provider channel and its on-device batch import share one normalized write, while file extraction writes straight to the store and keeps the report's own indicator names for semantic search to reconcile. See [Data Flow](/en/concepts/data-flow). ## Before / after What you send to `/v1/data` (or what `/v1/standardize` reads from a report) vs. what the structured store holds: | | Before (as written) | After (standardized) | | --- | --- | --- | | Indicator | `"血糖(空腹)"` / `"FBG"` / `"Glucose, fasting"` | `loinc_code: "1558-6"`, `canonical_name: "Fasting glucose [Mass/volume] in Serum or Plasma"` | | Value | `"97 mg/dL"` (one string) | `value: "97"` (raw value, no unit), `parsed_value: "97"`, `parsed_unit: "mg/dL"` (UCUM) | | Unit spelling | `"mg/dl"`, `"MG/DL"`, `"mg/dL"` | `"mg/dL"` (one UCUM form) | | Interop | free text | FHIR R4 Observation (`fhir_resource_id`) | | Original | — | **kept**: `value` holds the raw value as written (e.g. `97`, no unit appended — the unit rides in `parsed_unit`); raw names/units preserved | Three spellings of the same test become **one series** — trend queries, the agent's data tools, and your own analytics all see it as one indicator. ## Standardized output in the API | Surface | What appears | | --- | --- | | [`POST /v1/data`](/en/api-reference/data) | Response counts `standardized` alongside `ingested`. | | [`GET /v1/data`](/en/api-reference/data#read-records) | Every row carries `parsed_value` / `parsed_unit` / `loinc_code` / `canonical_name` / `fhir_resource_id`. | | [`POST /v1/standardize`](/en/api-reference/extract) | The whole pipeline as a synchronous call — **dry-run by default**, so you can inspect standardization before committing a write. | | The agent | Grounded answers query the standardized series — which is why "how's my glucose?" finds records written as "FBG". | ## Cookbook: one call, report → structured data ```bash # Inspect first (nothing persisted) … curl https://api.mirobody.ai/v1/standardize \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -F "user=alice" -F "file=@lab_report.pdf" # … then commit the same extraction curl https://api.mirobody.ai/v1/standardize \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -F "user=alice" -F "file=@lab_report.pdf" \ -F "store=true" -F "retention=permanent" ``` See [Standardize a report](/en/api-reference/extract) for the full row shape (`indicator_raw`, `loinc_code`, `confidence`, …). ## See also - [Structured Records](/en/api-reference/data) — the endpoint most writes go through. - [Narrative Text & Reports](/en/api-reference/extract) — running the same pipeline on a document. - [Models](/en/api-reference/models) — what the answer layer runs on. --- # Data Lifecycle https://docs.mirobody.ai/en/api-reference/lifecycle Retention, session cleanup, and Subject offboarding — how data leaves the platform. Data flows in as structured records and as files. Records are [standardized](/en/api-reference/standardization); uploaded files keep both their original and their extracted text. Either can ground the agent's answers. This page closes the loop — how data **leaves**, on your terms. Three exits, from automatic to total: 1. **Retention expiry** — time-bounded writes (`1h` / `2h` / `6h` / `1d`) expire on their own; see [Data retention](/en/api-reference/overview#data-retention). 2. **Session deletion** — one call tears down a named working scope (below). 3. **Subject offboarding** — one call erases everything about a Subject ([below](#offboarding-a-subject)). ## Sessions A `session_id` can name two things at once — a working-data scope and an Agent API conversation: 1. **Scopes `retention=session` data** — records written with `retention: "session"` via [`POST /v1/data`](/en/api-reference/data) (or [`POST /v1/standardize`](/en/api-reference/extract) with `store=true`) **must** carry a `session_id`. They live until the session is deleted, and deleting it is the only thing that clears them. 2. **Names a conversation on the Agent API** — passing `session_id` to [`POST /v1/responses`](/en/api-reference/responses) makes that conversation durable and resumable under the id. See [State & memory](/en/api-reference/state-and-memory). ```http DELETE /v1/sessions/{session_id} Authorization: Bearer mb_live_* ``` Ends the working-data scope: records written with `retention=session` under this `session_id` are erased (together with their FHIR mirrors), session-scoped file uploads are removed, and the underlying chat session is marked closed. This endpoint does **not** delete stored response objects. A response created with the same `session_id` remains available through `GET /v1/responses/{id}` and can still be chained. Delete the response objects with `DELETE /v1/responses/{id}`, or use Subject offboarding to erase every stored response for that Subject. ```bash curl -X DELETE "https://api.mirobody.ai/v1/sessions/sess_abc123?user=alice" \ -H "Authorization: Bearer $MIROBODY_API_KEY" ``` Pass the same `user` you used when writing the session's data; omitting it targets your account's default Subject. ```json { "status": "ok", "session_id": "sess_abc123", "deleted": 2, "subject": "alice" } ``` `deleted` counts session-scoped records, session-scoped files, and the underlying chat session. It does not count stored response objects. `0` means none of those resources matched. ```python import requests BASE = "https://api.mirobody.ai/v1" H = {"Authorization": "Bearer mb_live_..."} # Write working data scoped to the session requests.post(f"{BASE}/data", headers=H, json={ "user": "alice", "retention": "session", "session_id": "sess_abc123", "records": [{"indicator": "systolic_bp", "value": 148, "unit": "mmHg", "time": "2026-06-16T08:00:00Z"}], }) # ... run agent turns with session_id="sess_abc123" ... # When the interaction ends, purge its session-scoped working data: requests.delete(f"{BASE}/sessions/sess_abc123", headers=H, params={"user": "alice"}) ``` ## Offboarding a Subject ```http DELETE /v1/subjects/{user} Authorization: Bearer mb_live_* ``` Right to be forgotten in a single call. It erases **everything** about one Subject: 1. **Structured records** — everything written via [`POST /v1/data`](/en/api-reference/data), plus stored extractions, whatever their `retention` — FHIR resources included. 2. **Files** — everything uploaded via [`POST /v1/files`](/en/api-reference/files), immediately gone from the API. 3. **Stored Agent API conversations** — immediately unreadable (`GET /v1/responses/{id}` returns `404`) and then permanently deleted. 4. **The identity mapping** — the Subject itself becomes unreachable. A later request that passes the same `user` [mints a fresh, empty Subject](/en/api-reference/overview#multi-tenancy-the-user-field) with no connection to the erased one. ```bash curl -X DELETE "https://api.mirobody.ai/v1/subjects/alice" \ -H "Authorization: Bearer $MIROBODY_API_KEY" ``` An erase never creates the very Subject it is erasing: resolution looks up the existing mapping only. Passing a `user` your account has never used returns `404` — nothing is minted. ```json { "status": "ok", "subject": "alice", "deleted": { "records": 12, "files": 3, "conversations": 5 } } ``` | Field | Description | | --- | --- | | `deleted.records` | Structured health records erased. | | `deleted.files` | Files removed from the API surface. | | `deleted.conversations` | Stored Agent API conversations removed. | For **finer-grained** deletion, reach for the per-resource deletes instead: `DELETE /v1/data` (records), `DELETE /v1/files/{key}` (one file), `DELETE /v1/responses/{id}` (one stored response), `DELETE /v1/sessions/{id}` ([session-scoped data and files](#sessions)). See [Compliance](/en/api-reference/compliance) for the full user-rights picture. ## Errors | HTTP | When | | --- | --- | | `404` | `DELETE /v1/subjects/{user}`: your account has no Subject for this `user` — including one already erased | ## See also - [Structured Records](/en/api-reference/data) — writing and erasing structured records. - [State & Memory](/en/api-reference/state-and-memory) — what `store` and `retention` each control. - [Privacy & Compliance](/en/api-reference/compliance) — the controls behind these operations. --- # Agent API (Responses) https://docs.mirobody.ai/en/api-reference/responses POST /v1/responses — OpenAI Responses-compatible agent surface: client tools, stored conversations, previous_response_id chaining. import BaseUrl from '/snippets/base-url.mdx'; import SubjectUserParam from '/snippets/subject-user-param.mdx'; import StoreRetentionMatrix from '/snippets/store-retention-matrix.mdx'; ## Endpoint ```http POST /v1/responses Create a response GET /v1/responses/{response_id} Retrieve a stored response DELETE /v1/responses/{response_id} Delete a stored response; tear down its conversation if it was the last live response Authorization: Bearer mb_live_* ``` The **Agent API** speaks the [OpenAI Responses protocol](https://platform.openai.com/docs/api-reference/responses) — the **recommended way to build agents** on Mirobody. It does everything the [Answers API](/en/api-reference/chat) does (grounded answers over the Subject's real health data, server-side tools, reasoning), plus: - **Client function tools** — inject your own tools; the model hands off via `function_call` output items ([Function calling](/en/api-reference/function-calling)). - **Stored conversations** — `store` defaults to `true`; chain turns with `previous_response_id` or bind a durable conversation with `session_id` ([State & memory](/en/api-reference/state-and-memory)). - **Standard `response.*` streaming events** ([Streaming](/en/api-reference/streaming)). Because it speaks that protocol, the **[openai-agents SDK](https://github.com/openai/openai-agents-python) needs only a base-URL change**: ```python from agents import Agent, ModelSettings, Runner, set_default_openai_client, set_tracing_disabled from openai import AsyncOpenAI set_default_openai_client(AsyncOpenAI( base_url="https://api.mirobody.ai/v1", api_key="mb_live_...")) set_tracing_disabled(True) # tracing would call api.openai.com agent = Agent(name="Health assistant", model="mirobody-flash", model_settings=ModelSettings(extra_body={"user": "alice"})) print(Runner.run_sync(agent, "How is my fasting glucose trending?").final_output) ``` ## Create a response ```bash curl curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mirobody-flash", "input": "How is my fasting glucose trending?", "user": "alice" }' ``` ```python Python (OpenAI SDK) from openai import OpenAI client = OpenAI(api_key="mb_live_...", base_url="https://api.mirobody.ai/v1") resp = client.responses.create( model="mirobody-flash", input="How is my fasting glucose trending?", user="alice", ) print(resp.output_text) ``` ### Request body | Field | Type | Description | | --- | --- | --- | | `model` | string | `mirobody-flash` (default) or `mirobody-expert`. See [Models](/en/api-reference/models). | | `input` | string \| array | **Required.** A string (one user turn) or an array of items: `message` items (`{role, content}`), and — when continuing a tool handoff — `function_call` / `function_call_output` items. See [Function calling](/en/api-reference/function-calling). | | `instructions` | string | System-level instruction for this turn (prepended as a system message). | | `stream` | bool | `true` → SSE of standard `response.*` events. See [Streaming](/en/api-reference/streaming). | | `store` | bool | **Default `true`.** Persist the response + conversation: 30-day TTL, or permanent when bound to a `session_id`. Enables `previous_response_id` and `GET /v1/responses/{id}`. | | `previous_response_id` | string | Continue the conversation of a stored response (server-side state — no need to resend history). `404` if the id is unknown, expired, or deleted. | | `session_id` | string | Mirobody extension. Binds this and future turns to a **durable named conversation** (never auto-expires). Generate a globally unique opaque id (for example a UUID) and never reuse it across Subjects. | | `tools` | array | Client **function** tools (`{"type": "function", "name", "description", "parameters"}`; the completions-nested form is also accepted) and/or remote **MCP** servers (`{"type": "mcp", "server_label", "server_url", …}` — executed server-side). Max 64; names must match `[a-zA-Z0-9_-]{1,64}` and may not shadow built-in tool names. See [Function calling](/en/api-reference/function-calling) · [MCP servers](/en/api-reference/mcp-servers). | | `tool_choice` | string \| object | `"auto"` (default) / `"none"` / `"required"` / a named **client** tool. `"required"` and named guarantees apply in [backbone mode](/en/api-reference/backbone-mode); the full contract (incl. the `400` cases) is there. | | `mode` | string | `"agent"` (default, full server-side agent) or `"model"` ([backbone mode](/en/api-reference/backbone-mode) — bare inference, no server tools/state). | | `builtin_tools` | string \| array | `"auto"` (default) / `"none"` / an allowlist of built-in tool names. Trims the server-side domain tools — see [backbone mode](/en/api-reference/backbone-mode). | | `text.format` | object | Structured output (`json_object` / `json_schema`). **Backbone mode only** — agent mode returns `400`. See [Structured output](/en/api-reference/structured-output). | | `strict` | bool | When `true` (or header `X-Mirobody-Strict: 1`), an unknown top-level parameter is rejected with `400 unsupported_parameter` instead of being ignored. See [Strict validation](#strict-validation). | | `user` | string | Tenant-isolation key → a Subject. | ### Response object ```jsonc { "id": "resp_147e9b14c172429a8b19da4be9489243", "object": "response", "created_at": 1783741077, "status": "completed", "model": "mirobody-flash", "output": [ { "type": "reasoning", "id": "rs_0", "status": "completed", "summary": [{ "type": "summary_text", "text": "..." }] }, { "type": "message", "id": "msg_0", "status": "completed", "role": "assistant", "content": [{ "type": "output_text", "text": "Your fasting glucose has trended down ..." }] } ], "output_text": "Your fasting glucose has trended down ...", "usage": { "input_tokens": 9, // YOUR visible input only "output_tokens": 5, "total_tokens": 14, "input_tokens_details": { "system_tokens": 10096, "cached_tokens": 0 }, "output_tokens_details": { "reasoning_tokens": 0 }, "billed_tokens": { "input": 20391, "output": 841, "total": 21232 } // the actual token total you're metered on }, "tool_steps": [], // Mirobody extension: server tool trace "health_records": [], // Mirobody extension: {tool, data} evidence "citations": [], // Mirobody extension: literature evidence "previous_response_id": null, "store": true, "tools": [], "error": null, "metadata": {} } ``` The `output` array contains **only standard OpenAI item types** — `reasoning`, `message`, and (on a client-tool handoff) `function_call`. Official SDKs parse it as-is. **Server-side built-in tool runs are deliberately *not* output items.** The trace rides in the **top-level `tool_steps` extension field** (`{id, name, arguments, result}` — the same shape as the Answers API), which SDKs safely ignore. In streaming it surfaces as the side-channel event [`response.mirobody_tool_call`](/en/api-reference/streaming#server-tool-side-channel). `health_records` and `citations` carry the evidence the answer used, exactly as on the [Answers API](/en/api-reference/chat#response). ### Usage accounting `usage.input_tokens` reports the input **you actually sent**; the platform's system prompt / tool-schema overhead is broken out as `input_tokens_details.system_tokens`. `output_tokens_details.reasoning_tokens` reports provider reasoning tokens. Counts are summed across every model call of the agent turn. `usage.billed_tokens` (`{input, output, total}`) is what you're metered on — present on both the Agent API and the [Answers API](/en/api-reference/chat). Applying the catalog's standard input/output rates gives an estimate; prompt-cache discounts can make actual metered cost lower. ## Retrieve a stored response ```bash curl https://api.mirobody.ai/v1/responses/resp_147e9b14... \ -H "Authorization: Bearer $MIROBODY_API_KEY" ``` Returns the stored response object. Only `store=true` responses are retrievable; expired (30-day TTL) or deleted responses return `404`. ## Delete a stored response ```bash curl -X DELETE https://api.mirobody.ai/v1/responses/resp_147e9b14... \ -H "Authorization: Bearer $MIROBODY_API_KEY" ``` ```json { "id": "resp_147e9b14...", "object": "response.deleted", "deleted": true } ``` If it was the **last** live response of its conversation, the whole conversation is torn down too — its history and any conversation-derived memory. That makes this the self-service right-to-be-forgotten lever for stored conversations. ## State model Full details — TTLs, chaining semantics, stateless replay, and the cross-session memory that `store=true` feeds — in [State & memory](/en/api-reference/state-and-memory). ## Strict validation By default the surface is **lenient** (compatibility-first) — an unknown top-level parameter is ignored. Set `strict: true` in the body **or** send the header `X-Mirobody-Strict: 1` to make it a hard `400 unsupported_parameter` instead, with `param` pointing at the first offending key. Both surfaces (`/v1/responses` and `/v1/chat/completions`) honor it. Turn it on while integrating to catch typos and misplaced fields early. ```bash curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Mirobody-Strict: 1" \ -d '{"model":"mirobody-flash","input":"hi","bogus_param":1,"user":"alice"}' # → 400 {"error":{"code":"unsupported_parameter","param":"bogus_param", ...}} ``` `reasoning` (e.g. `{"effort": "low" | "medium" | "high"}`) is **honored**: the effort maps to the underlying model's extended-thinking / reasoning budget. It streams as the `response.reasoning_summary_text.*` events and lands in the non-stream `reasoning` output item; `usage.output_tokens_details.reasoning_tokens` counts it. Omit `reasoning` for the model's default behavior. Models without a thinking mode ignore it (no error). ## Errors Standard [error envelope](/en/api-reference/overview#error-format). Surface-specific cases: | HTTP | When | | --- | --- | | `400` | Missing/empty `input`; invalid `tools` (bad name, reserved name, duplicate, > 64, unsupported type, unreachable MCP server); unsupported `tool_choice` (see the [contract](/en/api-reference/backbone-mode)); `text.format` on agent mode; `mode:"model"` with `previous_response_id`/`session_id`/MCP tools; unknown `builtin_tools` name; unknown top-level param under `strict`; malformed handoff continuation (see [Function calling](/en/api-reference/function-calling#continuation-errors)) | | `404` | Unknown / expired / deleted `previous_response_id` or `response_id` | | `429` | `rate_limit_exceeded` (per-key rate limit) or `insufficient_quota` (monthly account cap) — see [Rate Limits & Quota](/en/api-reference/rate-limits) | | `502` | Upstream agent error — transient; retry with backoff. In [backbone mode](/en/api-reference/backbone-mode) provider errors are classified (400 / 429 / 502) rather than a blanket 502. | ## See also - [Function Calling](/en/api-reference/function-calling) — declaring client tools and continuing after a handoff. - [State & Memory](/en/api-reference/state-and-memory) — `store`, session state and cross-session memory. - [Streaming](/en/api-reference/streaming) — the `response.*` event sequence. - [Backbone Mode](/en/api-reference/backbone-mode) — turning this endpoint into a bare inference backend. --- # Backbone Mode https://docs.mirobody.ai/en/api-reference/backbone-mode Run the Agent API as a bare LLM backend: mode:\"model\", the builtin_tools allowlist, and the full tool_choice contract. import BaseUrl from '/snippets/base-url.mdx'; **Backbone mode** turns [`POST /v1/responses`](/en/api-reference/responses) into a *bare inference backend* — an OpenAI-compatible LLM with client function tools, but **no** server-side agent runtime, tools, planning, or stored state. Use it when your own agent framework (LangChain, openai-agents, a custom loop) is the orchestrator and Mirobody is "the model." The default (`mode: "agent"`) is the whole Mirobody agent: server-side runtime, built-in health-data tools, planning, and stored conversations. Reach for backbone mode only when you own the orchestration. If you want grounded answers over the Subject's real health data, stay on the default agent mode — that's where the [built-in tools](/en/api-reference/function-calling#built-in-server-tools) live. ## `mode` ```jsonc { "mode": "agent" | "model" } // default "agent" ``` | | `mode:"agent"` (default) | `mode:"model"` (backbone) | | --- | --- | --- | | Runtime | planning, sub-agents, code eval, virtual filesystem | single bare-model inference | | Server tools | built-in domain tools (trim with `builtin_tools`) | **none** | | System prompt | full agent base (~10k tokens) | minimal safety base (<100 tokens) | | State | `previous_response_id` / `session_id` threads | **stateless** (both → `400`) | | Continuation | stateful resume **or** stateless full replay | stateless full replay only | | Client tools | handoff via `function_call` (server-issued `call_id`) | bound directly to the model (**`call_id` preserved**) | | MCP tools | supported (server-executed) | `400` (no server tool loop) | | Server-side thread state | yes (deleted at request end when `store=false`) | none | | [`text.format`](/en/api-reference/structured-output) | `400` (not supported) | **supported** | With `store=true`, model mode still persists the response object (`GET /v1/responses/{id}` works), but the conversation is tagged as model-mode: a later `previous_response_id` against it returns an explicit `400` ("replay the transcript to continue") rather than silently resuming an empty thread. ```bash curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mirobody-flash", "mode": "model", "builtin_tools": "none", "input": "Summarize the attached lab panel.", "user": "alice" }' ``` Model mode is **stateless**. Continue a run by resending the full item transcript in `input` (the [stateless full-replay path](/en/api-reference/function-calling) — what openai-agents does by default). `previous_response_id`, `session_id`, and MCP tools each return `400`. ## `builtin_tools` ```jsonc { "builtin_tools": "auto" | "none" | ["query_health_data", ...] } // default "auto" ``` Controls the server-side **domain-tool family** — the health, clinical-record, and external-evidence tools: `query_health_data` · `list_clinical_records` · `list_family_members` · `search_medical_evidence` · `read_source` The external-evidence surface accepts only `search_medical_evidence` and `read_source`. Arbitrary URL fetching is not available: `read_source` accepts refs returned by search (`pmid:`, `pmc:`, `nct:`, or `doi:`), not model-constructed URLs. | Value | Effect | | --- | --- | | `"auto"` (default) | All domain tools available — current behavior. | | `"none"` | Domain tools all hidden. **Required when your backbone caller brings its own data or tools** — otherwise the model prefers to query the (empty) Mirobody Subject and skips your client tool. | | `["name", ...]` | Allowlist. An unknown name returns `400 invalid_value` and lists the available set. | The server-side orchestration primitives (`write_todos`, filesystem, `task`, `eval`) belong to agent mode itself, **outside** this parameter's scope — they may still appear in `tool_steps`. For a **zero** server-tool trace, use `mode:"model"`. ## `tool_choice` `tool_choice` follows OpenAI semantics, with model mode adding hard guarantees the agent runtime cannot make: | `tool_choice` | `mode:"model"` | `mode:"agent"` (default) | | --- | --- | --- | | omitted / `"auto"` | Model decides | Model decides | | `"none"` | Plain text (auto-drops into model mode) | Same, when stateless; with `previous_response_id`/`session_id` → `400` | | `"required"` | **Guarantees** the output has at least one client `function_call` | `400 unsupported_parameter` (the agent runtime can't force a client-tool call) | | `{"type":"function","name":X}` | **Guarantees** only `X` is called (stray calls pruned) | Accepted but best-effort (not forced) | | `required` / named + empty `tools` | `400 invalid_value`, `param="tool_choice"` | Same | **`tool_choice:"none"` disables *all* tools**, including the built-in ones (OpenAI semantics), and internally degrades to model-mode plain-text generation. The old behavior — clearing only client tools while the built-ins ran on — is gone. ### Guarantees for forced tool calls `required` / a named tool is a hard guarantee, not a hint — if it can't be honored you get a `502`, never a silent downgrade. In streaming, a forced call is synthesized into the standard event sequence (`response.output_item.added` → `response.function_call_arguments.delta`/`.done` → `response.output_item.done`), identical to the non-streaming behavior. This works with LangChain `create_agent` under both `ToolStrategy(...)` (which auto-sends `tool_choice:"required"`) and `ProviderStrategy(...)`. ## Provider error classification (model mode) In model mode, provider exceptions are classified by retryability instead of a blanket `502`: | Provider side | Returned | Meaning | | --- | --- | --- | | `400` / `404` / `413` / `422` (params, over-length) | **`400`** `invalid_request_error` | The provider's actionable message is passed through (truncated). **Do not retry.** | | `429` | **`429`** `rate_limit_error` | `Retry-After` passed through or synthesized. | | `401` / `403` (platform-side credentials / quota) | **`502`** `upstream_error` | A platform fault, not your request — no internal detail leaked. | | `5xx` / timeout / transport | **`502`** `upstream_error` | Error type only; full text is logged with an `X-Request-Id`. | ## See also - [Structured output](/en/api-reference/structured-output) — `text.format` (model mode only). - [Function calling](/en/api-reference/function-calling) — declaring client tools and the two continuation paths. - [Streaming](/en/api-reference/streaming) — `response.*` events, including the server-tool side-channel. - [Rate Limits & Quota](/en/api-reference/rate-limits) — per-key rate limit and the monthly account quota. --- # Function Calling https://docs.mirobody.ai/en/api-reference/function-calling Built-in server tools + your own client function tools on the Agent API, with both handoff continuation styles. The [Agent API](/en/api-reference/responses) runs two kinds of tools: 1. **Built-in server tools** — the platform's health-data tools. They run **server-side**; you never execute them. Their trace is reported, not delegated. 2. **Client function tools** — tools **you** declare on the request. When the model wants one, the response hands off with a `function_call` output item; you execute it and continue the run. ## Built-in server tools The agent always has its platform toolset over the Subject's data (same catalog as the [Answers API](/en/api-reference/chat#built-in-tools)): - `query_health_data` — search and aggregate the Subject's records - `list_clinical_records` — list clinical documents / FHIR-backed records - `list_family_members` — resolve care-circle members the Subject is allowed to query - `search_medical_evidence` — search literature, guidelines/consensus, and registered trials (feeds `citations`) - `read_source` — read one search result by ref; arbitrary URL fetching is not supported The `/v1` agent's domain tools are read-only. It also runs internal planning and analysis tools. Every server-tool run lands in the response object's **top-level `tool_steps` extension** — never as an `output` item, which official SDKs would mis-parse — and in streaming as the [`response.mirobody_tool_call` side-channel event](/en/api-reference/streaming#server-tool-side-channel). ## Declaring client tools ```jsonc { "model": "mirobody-flash", "input": "Check my recent glucose and book a follow-up if it is trending up.", "user": "alice", "tools": [ { "type": "function", "name": "book_appointment", "description": "Book a clinic appointment for the end user.", "parameters": { "type": "object", "properties": { "date": { "type": "string", "description": "ISO date" } }, "required": ["date"] } } ] } ``` Rules (violations are explicit `400`s, never silently dropped): | Rule | Detail | | --- | --- | | Type | `"type": "function"` (client handoff) and `"type": "mcp"` (server-side remote tools) are supported. Both the flat Responses form and the completions-nested `{"type":"function","function":{...}}` form are accepted. `{"type": "mcp"}` attaches your remote MCP servers (server-side execution, no handoff) — see [MCP servers](/en/api-reference/mcp-servers). | | Count | Max **64** tools per request. | | Names | Must match `[a-zA-Z0-9_-]{1,64}`; must be unique; must **not shadow a built-in tool name** (`query_health_data`, `read_file`, `task`, …). | | `parameters` | A JSON Schema object (defaults to `{"type":"object","properties":{}}`). | | `tool_choice` | `"auto"` (default), `"none"` (disables **all** tools for the turn, built-ins included — see [backbone mode](/en/api-reference/backbone-mode)), or a named client tool. Anything else → `400 unsupported_parameter`. | **Security note:** the agent holds tools over the Subject's health data. Subject isolation already limits every call to that developer's own data, but treat your tool descriptions and results as part of the prompt surface — don't feed untrusted third-party text through them without review. ## The handoff When the model calls your tool, the response **completes** with a `function_call` output item (`status: "completed"` — the *response* is done; the *conversation* is waiting on you): ```jsonc { "id": "resp_abc...", "object": "response", "status": "completed", "output": [ { "type": "function_call", "id": "fc_0", "call_id": "call_9f2...", "status": "completed", "name": "book_appointment", "arguments": "{\"date\": \"2026-07-14\"}" } ], "output_text": "", ... } ``` In streaming, the same handoff arrives as a `response.output_item.added` → `response.function_call_arguments.delta` / `.done` → `response.output_item.done` event group ([Streaming](/en/api-reference/streaming)). Execute the tool, then continue the run in **either** of two ways: ### Path 1 — stateful resume (`previous_response_id`) Send **only** `function_call_output` items, referencing the handoff response. The server resumes the paused agent thread — no history resend: ```bash curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mirobody-flash", "previous_response_id": "resp_abc...", "input": [ { "type": "function_call_output", "call_id": "call_9f2...", "output": "Booked: Mon 2026-07-14 09:30, Dr. Chen" } ], "user": "alice" }' ``` Requirements: outputs must cover **exactly** the pending `call_id`s (parallel calls → one `function_call_output` each); you may not mix `message` items into a resume; a handoff can be resumed **once** (a duplicate resume fails loudly). The handoff response must have been stored (`store=true`, the default). ### Path 2 — stateless full replay What **openai-agents** does by default: resend the *entire* item transcript in `input` — including the `function_call` / `function_call_output` pairs — with **no** `previous_response_id`: ```jsonc { "model": "mirobody-flash", "input": [ { "role": "user", "content": "Check my recent glucose and book a follow-up if it is trending up." }, { "type": "function_call", "call_id": "call_9f2...", "name": "book_appointment", "arguments": "{\"date\": \"2026-07-14\"}" }, { "type": "function_call_output", "call_id": "call_9f2...", "output": "Booked: Mon 2026-07-14 09:30, Dr. Chen" } ], "tools": [ ... ], "user": "alice" } ``` The pairs are reconstructed as conversation history and the run continues as a fresh turn. Works with `store=false` end to end. ## End-to-end with openai-agents The SDK handles the whole loop — declaration, handoff, execution, replay: ```python from agents import Agent, ModelSettings, Runner, function_tool, set_default_openai_client, set_tracing_disabled from openai import AsyncOpenAI set_default_openai_client(AsyncOpenAI( base_url="https://api.mirobody.ai/v1", api_key="mb_live_...")) set_tracing_disabled(True) @function_tool def book_appointment(date: str) -> str: """Book a clinic appointment for the end user (ISO date).""" return f"Booked: {date} 09:30, Dr. Chen" agent = Agent( name="Health assistant", model="mirobody-flash", instructions="Check real health data before acting.", tools=[book_appointment], model_settings=ModelSettings(extra_body={"user": "alice"}), # tenant isolation — REQUIRED ) result = Runner.run_sync(agent, "Check my recent glucose and book a follow-up if it's trending up.") print(result.final_output) ``` Every agents-SDK agent that touches Mirobody tools must pass the Subject via `model_settings=ModelSettings(extra_body={"user": ...})`. Without it, the run reads the **account-default Subject**, not the user you meant. The model reads the Subject's real glucose data with **built-in server tools**, then hands off to **your** `book_appointment` — the SDK executes it locally and replays the transcript automatically. ## Continuation errors | HTTP `400` message | Cause | | --- | --- | | `previous response has no pending function calls` | Resuming a response that wasn't a handoff (or was already resumed). | | `cannot mix message items with function_call_output when resuming via previous_response_id` | A resume must contain only tool outputs. | | `unknown call_id(s): [...]` / `missing function_call_output for call_id(s): [...]` | Outputs must match the pending calls exactly. | | `previous response has pending function call(s) — provide function_call_output items for: ...` | Continuing a handoff conversation without supplying the outputs. | | `function_call_output without matching function_call items — replay the full transcript, or resume via previous_response_id` | A stateless replay must include the `function_call` items too. | ## See also - [Agent API (Responses)](/en/api-reference/responses) — the endpoint these tools run on. - [MCP Servers](/en/api-reference/mcp-servers) — attaching a remote MCP server instead of a local tool. - [Structured Output](/en/api-reference/structured-output) — constraining the final answer to a schema. --- # Structured Output https://docs.mirobody.ai/en/api-reference/structured-output text.format — constrain the model to JSON (json_object) or a JSON Schema (json_schema) in backbone mode. Use `text.format` on [`POST /v1/responses`](/en/api-reference/responses) to request JSON output. It follows the [OpenAI Responses `text.format` shape](https://platform.openai.com/docs/api-reference/responses/create#responses-create-text) and is available **only in [backbone mode](/en/api-reference/backbone-mode) (`mode:"model"`)**. `text.format` requires `mode:"model"`. On the default agent mode it returns `400 unsupported_parameter` (`param: "text"`). The Answers API (`/v1/chat/completions`) likewise rejects `response_format` with `400`. ## `json_object` Constrains the output to a syntactically valid JSON object (no schema): ```jsonc { "model": "mirobody-flash", "mode": "model", "input": "Reply with a JSON object having keys a and b.", "text": { "format": { "type": "json_object" } } } ``` The message text is a JSON string you parse yourself: ```jsonc { "output_text": "{\"a\":1,\"b\":2}", "status": "completed", ... } ``` ## `json_schema` Requests output that follows a schema you supply. `strict: true` is forwarded to providers that support native JSON Schema: ```bash curl curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mirobody-flash", "mode": "model", "input": "Give me a manifest for apples with count 3.", "text": { "format": { "type": "json_schema", "name": "manifest", "strict": true, "schema": { "type": "object", "properties": { "item": { "type": "string" }, "count": { "type": "integer" } }, "required": ["item", "count"], "additionalProperties": false } } } }' ``` ```python Python (OpenAI SDK) from openai import OpenAI client = OpenAI(api_key="mb_live_...", base_url="https://api.mirobody.ai/v1") resp = client.responses.create( model="mirobody-flash", extra_body={"mode": "model"}, input="Give me a manifest for apples with count 3.", text={ "format": { "type": "json_schema", "name": "manifest", "strict": True, "schema": { "type": "object", "properties": {"item": {"type": "string"}, "count": {"type": "integer"}}, "required": ["item", "count"], "additionalProperties": False, }, } }, ) print(resp.output_text) # {"item":"apples","count":3} ``` ## Provider fallback `text.format` passes through to the underlying model. If the provider rejects native `json_schema`, Mirobody retries once as `json_object` with the schema included in the system prompt. That fallback targets the requested shape but does **not** validate the returned object against your schema. Parse and validate `output_text` yourself before using it. ## See also - [Backbone mode](/en/api-reference/backbone-mode) — `mode:"model"`, the prerequisite for `text.format`. - [Function calling](/en/api-reference/function-calling) — for tool-argument shaping, pair `text.format` with `tool_choice:"required"`. --- # MCP Servers https://docs.mirobody.ai/en/api-reference/mcp-servers Bring your own remote MCP tools into the agent turn — OpenAI-native tools entry on the Agent API. The agent ships with built-in tools over the Subject's health data ([Function calling → Built-in server tools](/en/api-reference/function-calling#built-in-server-tools)). Attaching a remote MCP server adds **your own tools** to that set: the platform connects, lists the server's tools, and lets the agent call them alongside the built-ins during the turn. This is **not** "Mirobody as an MCP server." It's the reverse: you bring external MCP tools *into* the agent's reasoning. The platform executes the calls server-side — unlike [client function tools](/en/api-reference/function-calling), there is no handoff back to you. ## Attach a server Use the OpenAI Responses protocol's own `tools` entry, on the **Agent API**: ```bash curl https://api.mirobody.ai/v1/responses \ -H "Authorization: Bearer $MIROBODY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mirobody-expert", "input": "Cross-check my recent labs against our formulary.", "user": "alice", "tools": [ { "type": "mcp", "server_label": "formulary", "server_url": "https://mcp.your-company.com/mcp", "authorization": "YOUR_TOKEN", "allowed_tools": ["lookup_drug"] } ] }' ``` | Field | Notes | | --- | --- | | `server_label` | Required. Alphanumeric/`_`/`-`; shows up in `tool_steps` names as `mcp__{label}__{tool}`. | | `server_url` | Required. **Public http(s)** streamable-HTTP MCP endpoint — hosts resolving to private/loopback ranges are rejected (`400`). | | `authorization` | Optional. Sent as the `Authorization` header (auto-prefixed `Bearer ` unless you pass a scheme). | | `allowed_tools` | Optional allowlist of tool names; everything else on the server is ignored. | | `require_approval` | Only `"never"` (default). `"always"` returns `400` — the server is yours and calls run server-side, so an approval round-trip adds nothing. | Mix freely with your `type: "function"` client tools in the same `tools` array. Limits: ≤ 8 servers per request, ≤ 64 tools total. ## The response MCP calls are **server-side tool steps** — they appear in the response's top-level `tool_steps` (name `mcp__{label}__{tool}`, with arguments and result) and stream as `response.mirobody_tool_call` events. The `output` array stays pure OpenAI item types; there is no `function_call` handoff for MCP tools. ## Errors | HTTP `400` message | Cause | | --- | --- | | `mcp server '