mobieusKnow mobieusCore API — mobieusHealth History #548
Author
Patrick Bass
Submitted
Aug 27, 2026 10:48am
Reviewed
Aug 27, 2026 10:48am
Summary
Render numbered steps and the notice-status table properly, and drop the heading that repeats the page title
+ 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.
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.
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
### 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.
The pantry is a preference the meal suggester leans on, not a filter it obeys. Suggestions are never restricted to it. Names are normalized 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
### 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.
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"`.
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 labeled `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.
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
## 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.
- 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 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. |
| `GET` | `/api/v1/health/summary` | `health:read` | The owner's computed snapshot of 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/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/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]}`, taking 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:
Behavior 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.
- **Shopping quantities are arithmetic, not description.** Identical ingredients merge, units normalize 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. |
| `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.
- `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 carries the `disclaimers` array. It is always non-empty and the allergen notice is always first.
- The request body takes `restaurant`, `locality` and `mode`. Clients sending `restaurant_id`, `restaurant_name` or `location` should move to these field names.
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`.
See also: [mobieusCore API Overview](/know/api-overview), [Authentication, scopes, and rate limits](/know/api-authentication), 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

# 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. 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`.