The Health surface of the public REST API. A key reads and logs its own owner's mobieusHealth record — daily summary, weight, vitals, water, and activity. This surface is deliberately single-user: a key never sees, and can never address, any other member's health data.
Added in API version 1.19.0.
New in 1.25.0
Four additions, all on the same single-user model — a key reads and writes only its own owner's record.
Pantry — the basics a member keeps on hand
| Method | Path | Scope |
|---|---|---|
GET |
/api/v1/health/food/pantry |
health:read |
POST |
/api/v1/health/food/pantry |
health:write |
POST |
/api/v1/health/food/pantry/{id} |
health:write |
POST |
/api/v1/health/food/pantry/{id}/delete |
health:write |
POST |
/api/v1/health/food/pantry/clear |
health:write |
The pantry is a preference the meal suggester leans on, not a filter it obeys — suggestions are never restricted to it. Names are normalised and deduplicated per member; adding a name that already exists returns the existing item rather than a second row.
Menu scan — read a photographed menu
| Method | Path | Scope |
|---|---|---|
POST |
/api/v1/health/eating-out/scan-menu |
health:eating_out |
Multipart: photo (JPEG, PNG, HEIC or WebP, 12 MB max) plus mode. Returns the same shape a looked-up restaurant returns, because it runs through the same ranker — including the contains / likely_contains / unclear restriction vocabulary and the standing allergen disclaimer. There is no value in that vocabulary meaning "free of", here or anywhere.
Resolves synchronously; unlike /suggestions there is no pending/job_id branch. The uploaded photo is not stored — it is read, ranked, and unlinked before the response is written. Calorie figures on a photographed menu are normally estimated rather than published, and are labelled confidence: "estimated".
Progress photo date
| Method | Path | Scope |
|---|---|---|
POST |
/api/v1/health/photos/{id}/date |
health:write |
Body { "photo_date": "YYYY-MM-DD" }. Moves a photo to the day it was taken. Future dates are rejected.
Target-date planning
| Method | Path | Scope |
|---|---|---|
POST |
/api/v1/health/forecast/target-date |
health:read |
Body { "target_date": "YYYY-MM-DD" }. Plans backwards from a date to the daily intake that reaches it. The search is bounded by the safety floor rather than clamped to it, so an unreachable date returns feasible: false with the earliest date that is reachable — never an unsafe intake.
Privacy model — read this first
mobieusHealth entries are encrypted per member, and the platform rule is that tenant admins manage configuration and see aggregates but never an individual member's raw entries. The API honors that rule exactly:
- Every endpoint operates on one user — the user who owns the API key. The owning user is taken only from the authenticated key, never from a path segment, query string, or request body.
- There is deliberately no
?user_id=parameter and no/health/users/{id}form. A key minted by a super admin returns the admin's own entries and nothing else. - Aggregate or admin reporting is a separate admin-plane concern and is not part of this API.
Scopes, plan, and seat gate
| Scope | Grants |
|---|---|
health:read |
Read the owner's daily summary, weight, vitals, water, and activity. |
health:write |
Log the owner's weight, vitals, and water. |
health:eating_out |
Look up a restaurant and get ranked on-plan orders, and report bad menu data. Its own scope rather than riding health:read, because it sends the owner's dietary restrictions to an outbound lookup. |
Generate a key at /admin/api-keys and tick the scopes you need. Like the rest of the public API, mobieusHealth endpoints are Pro / Creator Plus / Sovereign only.
Two extra gates apply on top of the plan gate:
- The tenant must have mobieusHealth enabled. If it doesn't, every
/api/v1/health/*path returns404— the surface is invisible, indistinguishable from a route that doesn't exist. - The key owner must hold an active mobieusHealth seat. Without one, calls return
403 health_no_access.
Quick start
# Today's totals vs goals, for the key owner
curl https://YOUR-TENANT.mobieus.io/api/v1/health/summary \\
-H 'Authorization: Bearer mc_live_...'
# Log this morning's weigh-in
curl -X POST https://YOUR-TENANT.mobieus.io/api/v1/health/weight \\
-H 'Authorization: Bearer mc_live_...' -H 'Content-Type: application/json' \\
-d '{"weight":182.4,"unit":"lb"}'
Resources
Summary
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/summary |
health:read |
The owner's computed snapshot — today's intake versus goals. Returns 409 not_onboarded if the owner holds a seat but hasn't finished mobieusHealth onboarding. |
Weight
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/weight?from=&to= |
health:read |
The owner's weight series (defaults to the last 90 days; from/to are YYYY-MM-DD). Each point carries weight_kg (canonical) and weight in the profile's unit. |
POST |
/api/v1/health/weight |
health:write |
Log a weigh-in. Body: {"weight_kg": number} or {"weight": number, "unit": "kg"\|"lb"} (unit defaults to the profile's). Optional date (defaults to today; not in the future). Returns 201. |
Vitals
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/vitals?from=&to= |
health:read |
The owner's vitals series (default last 30 days). The response lists the available fields. |
POST |
/api/v1/health/vitals |
health:write |
Merge-log a day's vitals. Body: any of systolic, diastolic, resting_hr, glucose, sleep_hours, energy, mood (numeric; at least one required), optional glucose_fasting (bool, with glucose), optional date. Returns 201. |
Water
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/water?from=&to= |
health:read |
The owner's daily water totals in fluid ounces (default last 30 days), plus water_goal_oz. |
POST |
/api/v1/health/water |
health:write |
Add to a day's running total. Body: {"amount": number} in fluid ounces (negative corrects an over-log; the total floors at 0), optional date. Mirrors the in-app quick-add. Returns 201. |
Activity
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/activity?from=&to= |
health:read |
The owner's daily activity series (default last 30 days): steps, active_minutes, total_calories, and the day's individual sessions (type, duration, calories). Read-only over the API; log sessions in the app. |
Range queries span at most 366 days; an out-of-range or malformed from/to is clamped to a valid window.
Meal planner
Added in 1.24.0. Seven endpoints, gated on the health_meal_planner feature flag as well as everything above. That flag is off by default; where it is off, every path here returns the same 404 a non-existent route returns, so a tenant without the planner cannot detect it.
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/planner |
health:read |
The active plan, its shopping list, the ticked lines, and the per-day macro report. data.plan is null when there is none. |
POST |
/api/v1/health/planner/generate |
health:write |
Body {"days": 1\|3\|5\|7, "include_snacks": bool}. Any other days value is 400 bad_days — it is not clamped. Replaces the active plan. |
POST |
/api/v1/health/planner/swap |
health:write |
Body {"day_index": int, "meal_type": string}. Replaces one meal, leaving the rest of the plan untouched. |
POST |
/api/v1/health/planner/shopping |
health:write |
Rebuilds the consolidated list. No model call — the merging and summing are deterministic. |
POST |
/api/v1/health/planner/shopping/check |
health:write |
Body {"checked": [string]} — item key values, not indexes. Replaces the set. |
POST |
/api/v1/health/planner/log |
health:write |
Body {"day_index": int, "meal_type": string, "date": "YYYY-MM-DD"}. Omit meal_type to log the whole day. Writes through the ordinary food-log path. |
POST |
/api/v1/health/planner/clear |
health:write |
Idempotent: no plan is a successful clear, not a 404. |
Behaviour worth knowing before you integrate:
- The read stays available when a subscription lapses.
GET /api/v1/health/planneris on the free rung; everything that generates, swaps or persists requires an entitlement and returns402. Every response carries acanblock (generate/swap/log/export) so a client learns it has gone read-only from the next call rather than from a refusal. - The request body carries no health data.
generatetakes a duration and a toggle. Macro targets, diet style and the member's exclusion list are resolved server-side from their own profile; there is deliberately no way to supply them, and therefore no way for a caller to assert its own constraints. - A member with no calorie target gets
409 targets_required. The planner does not build against defaults. - A plan that fails its constraint check is never returned. Exclusions and diet style are checked over the whole plan including every consolidated shopping line, before anything is stored. A violation is
422 plan_violates_constraintsand nothing is saved. - Shopping quantities are arithmetic, not description. Identical ingredients merge, units normalise within their family before summing, and two measures that cannot be honestly combined stay as separate lines.
Eating out
| Method | Path | Scope | What |
|---|---|---|---|
GET |
/api/v1/health/eating-out/restaurants |
health:eating_out |
Type-ahead over restaurants already looked up. |
POST |
/api/v1/health/eating-out/suggestions |
health:eating_out |
Body {"restaurant": string, "locality": string, "mode": "stay_on_plan"\|"best_available"}. Ranked orders against what is left of the owner's day. |
GET |
/api/v1/health/eating-out/suggestions/{job_id} |
health:eating_out |
Poll a lookup that had to research the menu. |
POST |
/api/v1/health/eating-out/report |
health:eating_out |
Added in 1.24.0. Report incorrect menu data. Body {"restaurant": string, "reason": string} — both required — plus optional locality, item_id, item_name. Files into the shared moderation queue; the cached menu is dropped once two distinct members have open reports against the same restaurant. |
Changes in 1.24.0:
- Suggestions now carry
fit_score(0-100), a deterministic one-linerationale, and themodethat produced them together withsuppressed_by_mode. modeon the request:stay_on_plan(the default) suppresses poor fits and reports how many;best_availablereturns them. Neither mode relaxes the allergen filter — an item flagged as containing a declared restriction is excluded under both, always.- Every response now carries the
disclaimersarray this reference has described since 1.23.0. It was documented and never sent. It is always non-empty and the allergen notice is always first. - The documented request body is corrected to the fields the handler reads —
restaurant,locality,mode. It previously listedrestaurant_id,restaurant_nameandlocation, none of which were ever implemented.
Ingredient and allergen information is estimated and may be incomplete or outdated. Nothing in this surface asserts that an item is free of anything; the vocabulary is contains, likely contains, and unclear.
Errors
These use the standard error envelope. The health-specific codes:
| HTTP | error.code |
Meaning |
|---|---|---|
| 403 | health_no_access |
The key owner has no active mobieusHealth seat. |
| 403 | no_key_owner |
The key isn't tied to a user, so it has no personal record. |
| 409 | not_onboarded |
The owner has a seat but hasn't completed onboarding. |
| 400 | invalid_weight |
POST /weight without a positive weight_kg or weight. |
| 400 | no_readings |
POST /vitals with no recognized reading. |
| 400 | invalid_amount |
POST /water without a non-zero numeric amount. |
| 400 | bad_days |
POST /planner/generate with a days value other than 1, 3, 5 or 7. |
| 409 | targets_required |
The owner has no calorie target, so the planner has nothing to build against. |
| 422 | plan_violates_constraints |
The generated plan failed its exclusion or diet check. Nothing was stored. |
| 422 | swap_violates_constraints |
The replacement meal failed the same check. The original meal is untouched. |
| 400 | restaurant_required |
POST /eating-out/report without a restaurant. |
| 400 | reason_required |
POST /eating-out/report without a reason. |
| 402 | premium_required |
The action needs an active entitlement. The envelope carries the feature key and its upsell copy. |
See also: mobieusCore API — Overview, Authentication, scopes, and rate limits, and the full machine reference at /api/docs.
The Health surface of the public REST API. A key reads and logs **its own owner's** mobieusHealth record — daily summary, weight, vitals, water, and activity. This surface is deliberately single-user: a key never sees, and can never address, any other member's health data.
Added in API version **1.19.0**.
## New in 1.25.0
Four additions, all on the same single-user model — a key reads and writes only its own owner's record.
### Pantry — the basics a member keeps on hand
| Method | Path | Scope |
|---|---|---|
| `GET` | `/api/v1/health/food/pantry` | `health:read` |
| `POST` | `/api/v1/health/food/pantry` | `health:write` |
| `POST` | `/api/v1/health/food/pantry/{id}` | `health:write` |
| `POST` | `/api/v1/health/food/pantry/{id}/delete` | `health:write` |
| `POST` | `/api/v1/health/food/pantry/clear` | `health:write` |
The pantry is a preference the meal suggester leans on, not a filter it obeys — suggestions are never restricted to it. Names are normalised and deduplicated per member; adding a name that already exists returns the existing item rather than a second row.
### Menu scan — read a photographed menu
| Method | Path | Scope |
|---|---|---|
| `POST` | `/api/v1/health/eating-out/scan-menu` | `health:eating_out` |
Multipart: `photo` (JPEG, PNG, HEIC or WebP, 12 MB max) plus `mode`. Returns the **same** shape a looked-up restaurant returns, because it runs through the same ranker — including the `contains` / `likely_contains` / `unclear` restriction vocabulary and the standing allergen disclaimer. There is no value in that vocabulary meaning "free of", here or anywhere.
Resolves synchronously; unlike `/suggestions` there is no pending/`job_id` branch. The uploaded photo is not stored — it is read, ranked, and unlinked before the response is written. Calorie figures on a photographed menu are normally **estimated** rather than published, and are labelled `confidence: "estimated"`.
### Progress photo date
| Method | Path | Scope |
|---|---|---|
| `POST` | `/api/v1/health/photos/{id}/date` | `health:write` |
Body `{ "photo_date": "YYYY-MM-DD" }`. Moves a photo to the day it was taken. Future dates are rejected.
### Target-date planning
| Method | Path | Scope |
|---|---|---|
| `POST` | `/api/v1/health/forecast/target-date` | `health:read` |
Body `{ "target_date": "YYYY-MM-DD" }`. Plans backwards from a date to the daily intake that reaches it. The search is **bounded by** the safety floor rather than clamped to it, so an unreachable date returns `feasible: false` with the earliest date that *is* reachable — never an unsafe intake.
## Privacy model — read this first
mobieusHealth entries are encrypted per member, and the platform rule is that tenant admins manage configuration and see aggregates but **never** an individual member's raw entries. The API honors that rule exactly:
- Every endpoint operates on **one** user — the user who owns the API key. The owning user is taken only from the authenticated key, never from a path segment, query string, or request body.
- There is deliberately **no** `?user_id=` parameter and no `/health/users/{id}` form. A key minted by a super admin returns the admin's own entries and nothing else.
- Aggregate or admin reporting is a separate admin-plane concern and is not part of this API.
## Scopes, plan, and seat gate
| Scope | Grants |
|---|---|
| `health:read` | Read the owner's daily summary, weight, vitals, water, and activity. |
| `health:write` | Log the owner's weight, vitals, and water. |
| `health:eating_out` | Look up a restaurant and get ranked on-plan orders, and report bad menu data. Its own scope rather than riding `health:read`, because it sends the owner's dietary restrictions to an outbound lookup. |
Generate a key at **`/admin/api-keys`** and tick the scopes you need. Like the rest of the public API, mobieusHealth endpoints are **Pro / Creator Plus / Sovereign** only.
Two extra gates apply on top of the plan gate:
- The tenant must have **mobieusHealth** enabled. If it doesn't, every `/api/v1/health/*` path returns `404` — the surface is invisible, indistinguishable from a route that doesn't exist.
- The **key owner must hold an active mobieusHealth seat**. Without one, calls return `403 health_no_access`.
## Quick start
```bash
# Today's totals vs goals, for the key owner
curl https://YOUR-TENANT.mobieus.io/api/v1/health/summary \\
-H 'Authorization: Bearer mc_live_...'
# Log this morning's weigh-in
curl -X POST https://YOUR-TENANT.mobieus.io/api/v1/health/weight \\
-H 'Authorization: Bearer mc_live_...' -H 'Content-Type: application/json' \\
-d '{"weight":182.4,"unit":"lb"}'
```
## Resources
### Summary
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/summary` | `health:read` | The owner's computed snapshot — today's intake versus goals. Returns `409 not_onboarded` if the owner holds a seat but hasn't finished mobieusHealth onboarding. |
### Weight
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/weight?from=&to=` | `health:read` | The owner's weight series (defaults to the last 90 days; `from`/`to` are `YYYY-MM-DD`). Each point carries `weight_kg` (canonical) and `weight` in the profile's unit. |
| `POST` | `/api/v1/health/weight` | `health:write` | Log a weigh-in. Body: `{"weight_kg": number}` **or** `{"weight": number, "unit": "kg"\\|"lb"}` (unit defaults to the profile's). Optional `date` (defaults to today; not in the future). Returns `201`. |
### Vitals
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/vitals?from=&to=` | `health:read` | The owner's vitals series (default last 30 days). The response lists the available `fields`. |
| `POST` | `/api/v1/health/vitals` | `health:write` | Merge-log a day's vitals. Body: any of `systolic`, `diastolic`, `resting_hr`, `glucose`, `sleep_hours`, `energy`, `mood` (numeric; at least one required), optional `glucose_fasting` (bool, with `glucose`), optional `date`. Returns `201`. |
### Water
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/water?from=&to=` | `health:read` | The owner's daily water totals in fluid ounces (default last 30 days), plus `water_goal_oz`. |
| `POST` | `/api/v1/health/water` | `health:write` | Add to a day's running total. Body: `{"amount": number}` in fluid ounces (negative corrects an over-log; the total floors at 0), optional `date`. Mirrors the in-app quick-add. Returns `201`. |
### Activity
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/activity?from=&to=` | `health:read` | The owner's daily activity series (default last 30 days): `steps`, `active_minutes`, `total_calories`, and the day's individual `sessions` (type, duration, calories). Read-only over the API; log sessions in the app. |
Range queries span at most 366 days; an out-of-range or malformed `from`/`to` is clamped to a valid window.
### Meal planner
Added in **1.24.0**. Seven endpoints, gated on the `health_meal_planner` feature flag as well as everything above. That flag is **off by default**; where it is off, every path here returns the same `404` a non-existent route returns, so a tenant without the planner cannot detect it.
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/planner` | `health:read` | The active plan, its shopping list, the ticked lines, and the per-day macro report. `data.plan` is `null` when there is none. |
| `POST` | `/api/v1/health/planner/generate` | `health:write` | Body `{"days": 1\\|3\\|5\\|7, "include_snacks": bool}`. Any other `days` value is `400 bad_days` — it is not clamped. Replaces the active plan. |
| `POST` | `/api/v1/health/planner/swap` | `health:write` | Body `{"day_index": int, "meal_type": string}`. Replaces one meal, leaving the rest of the plan untouched. |
| `POST` | `/api/v1/health/planner/shopping` | `health:write` | Rebuilds the consolidated list. No model call — the merging and summing are deterministic. |
| `POST` | `/api/v1/health/planner/shopping/check` | `health:write` | Body `{"checked": [string]}` — item `key` values, not indexes. Replaces the set. |
| `POST` | `/api/v1/health/planner/log` | `health:write` | Body `{"day_index": int, "meal_type": string, "date": "YYYY-MM-DD"}`. Omit `meal_type` to log the whole day. Writes through the ordinary food-log path. |
| `POST` | `/api/v1/health/planner/clear` | `health:write` | Idempotent: no plan is a successful clear, not a `404`. |
Behaviour worth knowing before you integrate:
- **The read stays available when a subscription lapses.** `GET /api/v1/health/planner` is on the free rung; everything that generates, swaps or persists requires an entitlement and returns `402`. Every response carries a `can` block (`generate` / `swap` / `log` / `export`) so a client learns it has gone read-only from the next call rather than from a refusal.
- **The request body carries no health data.** `generate` takes a duration and a toggle. Macro targets, diet style and the member's exclusion list are resolved server-side from their own profile; there is deliberately no way to supply them, and therefore no way for a caller to assert its own constraints.
- **A member with no calorie target gets `409 targets_required`.** The planner does not build against defaults.
- **A plan that fails its constraint check is never returned.** Exclusions and diet style are checked over the whole plan *including every consolidated shopping line*, before anything is stored. A violation is `422 plan_violates_constraints` and nothing is saved.
- **Shopping quantities are arithmetic, not description.** Identical ingredients merge, units normalise within their family before summing, and two measures that cannot be honestly combined stay as separate lines.
### Eating out
| Method | Path | Scope | What |
|---|---|---|---|
| `GET` | `/api/v1/health/eating-out/restaurants` | `health:eating_out` | Type-ahead over restaurants already looked up. |
| `POST` | `/api/v1/health/eating-out/suggestions` | `health:eating_out` | Body `{"restaurant": string, "locality": string, "mode": "stay_on_plan"\\|"best_available"}`. Ranked orders against what is left of the owner's day. |
| `GET` | `/api/v1/health/eating-out/suggestions/{job_id}` | `health:eating_out` | Poll a lookup that had to research the menu. |
| `POST` | `/api/v1/health/eating-out/report` | `health:eating_out` | Added in 1.24.0. Report incorrect menu data. Body `{"restaurant": string, "reason": string}` — both required — plus optional `locality`, `item_id`, `item_name`. Files into the shared moderation queue; the cached menu is dropped once two distinct members have open reports against the same restaurant. |
Changes in **1.24.0**:
- Suggestions now carry `fit_score` (0-100), a deterministic one-line `rationale`, and the `mode` that produced them together with `suppressed_by_mode`.
- `mode` on the request: `stay_on_plan` (the default) suppresses poor fits and reports how many; `best_available` returns them. **Neither mode relaxes the allergen filter** — an item flagged as containing a declared restriction is excluded under both, always.
- Every response now carries the `disclaimers` array this reference has described since 1.23.0. It was documented and never sent. It is always non-empty and the allergen notice is always first.
- The documented request body is corrected to the fields the handler reads — `restaurant`, `locality`, `mode`. It previously listed `restaurant_id`, `restaurant_name` and `location`, none of which were ever implemented.
Ingredient and allergen information is estimated and may be incomplete or outdated. Nothing in this surface asserts that an item is free of anything; the vocabulary is contains, likely contains, and unclear.
## Errors
These use the standard [error envelope](/know/api-authentication). The health-specific codes:
| HTTP | `error.code` | Meaning |
|---|---|---|
| 403 | `health_no_access` | The key owner has no active mobieusHealth seat. |
| 403 | `no_key_owner` | The key isn't tied to a user, so it has no personal record. |
| 409 | `not_onboarded` | The owner has a seat but hasn't completed onboarding. |
| 400 | `invalid_weight` | `POST /weight` without a positive `weight_kg` or `weight`. |
| 400 | `no_readings` | `POST /vitals` with no recognized reading. |
| 400 | `invalid_amount` | `POST /water` without a non-zero numeric `amount`. |
| 400 | `bad_days` | `POST /planner/generate` with a `days` value other than 1, 3, 5 or 7. |
| 409 | `targets_required` | The owner has no calorie target, so the planner has nothing to build against. |
| 422 | `plan_violates_constraints` | The generated plan failed its exclusion or diet check. Nothing was stored. |
| 422 | `swap_violates_constraints` | The replacement meal failed the same check. The original meal is untouched. |
| 400 | `restaurant_required` | `POST /eating-out/report` without a restaurant. |
| 400 | `reason_required` | `POST /eating-out/report` without a reason. |
| 402 | `premium_required` | The action needs an active entitlement. The envelope carries the feature key and its upsell copy. |
See also: [mobieusCore API — Overview](/know/api-overview), [Authentication, scopes, and rate limits](/know/api-authentication), and the full machine reference at `/api/docs`.