# PlateHerder nutrition API v1.2 — Claude / OWS handoff

Additive changes under https://plateherder.com/api/v1. No OWS code is changed. Google review remains paused and is tracked in docs/GOOGLE-VERIFICATION.md.

## Plate and portion contract

`servings` is the number of portions in the entire batch; it is the one authoritative serving count. `yield_grams` is optional edible/cooked batch weight, supplied by the user. `serving_label` is optional, e.g. "1 bowl". No cooked weight is inferred from raw ingredient weights. `nutrition.serving_size.grams` is yield_grams / servings when both are known. Ingredient quantities and grams always describe the whole batch.

`nutrition` is returned on plate list/detail and ready extraction drafts:

```json
{
  "per_serving": { "kcal": 610, "protein_g": 48, "carbs_g": null, "fat_g": null, "fiber_g": null },
  "confidence": "user_confirmed",
  "method": "user",
  "complete": true,
  "stale": false,
  "serving_size": { "grams": 320, "label": "1 bowl" },
  "computed_at": "2026-09-07T18:00:00.000Z",
  "missing_ingredients": [],
  "warnings": ["User-entered values; not independently verified."]
}
```

Confidence is `estimated`, `user_confirmed`, or `unknown`; never `verified`. Method is `ingredients`, `creator` (structured source claim), `user`, or `ai` for the explicit automatic-estimate fallback. `complete` means per-serving calories and protein are available, not that every nutrient is known or accuracy certified. `total` contains the same five fields for the whole recipe; `total_complete` indicates whole-recipe calories and protein are available even when servings are unknown. Missing carbs/fat/fiber remain null. Ingredient calculations require all ingredient weights and calorie/protein matches; partial totals are not returned as complete totals. Missing ingredient indices are zero-based and remain visible even with a user/source override. `calculation_version: 3` identifies this calculation format. Recipe totals and per-serving values are calculated before rounding separately to one decimal place. Creator/user totals multiply their per-serving values by the known serving count.

Source nutrition is parsed from recipe JSON-LD when an explicit numeric serving count is available. It is labelled as the creator's estimate, not independently verified. Normal source extraction does not generate macro numbers or guess unknown amounts. Explicit automatic estimation is described below. `source_nutrition` is server-controlled extraction provenance, not a way for clients to claim verified source nutrition.

## Food normalization

GET /foods?q=chicken%20breast%20raw returns up to 20 reference candidates. GET /foods/usda:171077 returns a reference food and source URL. Both need plates:read. The bundled USDA FoodData Central SR Legacy dataset contains 7,793 foods from its final April 2018 release, not a current branded-product database. Source data are public domain (CC0). No live USDA key is required for this snapshot.

Choose the food whose description matches preparation state, skin/bone content and edible portion. Search results are not a match-confidence ranking. Automatic matches use normalized descriptions and documented aliases. During extraction, one bounded AI pass may choose from supplied USDA candidates; low-confidence, duplicate, or out-of-candidate decisions are rejected. AI matches remain estimates, are labelled in warnings and can be corrected. The model never supplies nutrient numbers or estimated weights. If matching fails, the recipe still imports. There is no calibrated numeric confidence score.

Each ingredient preserves raw text and parses explicit quantities, fractions and units into qty/unit/item. Ranges and ambiguous packages stay unresolved. Ingredients add food_id, food_name, per_100g, grams, match_method and grams_method where available. POST/PATCH accept food_id and grams; the server resolves nutrient values from its reference data and ignores client-supplied per_100g tables. Unknown food IDs return 400. Mass units g/kg/oz/lb convert arithmetically. Household measures use food-specific USDA portion records, including explicit qualifiers such as chopped, sliced or egg size. Cup/tablespoon/teaspoon conversions use US customary proportions only. Unmatched portions, unspecified piece sizes, package sizes and metric volumes require a weight. `grams_method: portion` and `portion_label` identify estimated reference weights; `match_method: ai` identifies AI candidate selections. The user must account for drained liquids, inedible portions and cooking losses. User-weighed grams override portion estimates. Unchanged portion weights retain provenance, and changing the food or quantity recalculates the portion weight.

## Corrections and invalidation

PATCH /plates/:id with `{"nutrition":{"per_serving":{"kcal":610,"protein_g":48}}}` stores a user-confirmed correction for this user's plate only. Other nutrients default to null. Client assertions of provenance do not establish independent verification or modify the shared USDA reference. Optional yield_grams, serving_label and servings can be updated in the same request.

Ingredient or batch-serving/yield changes invalidate prior user/creator numbers. Until recalculated or re-entered, nutrition becomes stale/unknown and is excluded from nutrition filters. Changing notes or tags does not invalidate it. Changing ingredient text or quantities clears inherited matches/weights when they no longer safely describe that ingredient. Re-reading or editing unrelated metadata does not clear a stale flag.

PATCH `{"nutrition":null}` explicitly discards an override and recalculates from current ingredients. This can be combined with corrected food IDs/weights. If inputs remain incomplete, results remain unknown. Saving a different per_serving object explicitly confirms those numbers for the new basis; clients must not blindly resend old nutrition with a changed ingredient list. Unchanged full-response round trips do not promote estimates to user confirmation.

## OWS logging

For 1.5 servings, multiply each known per_serving nutrient by 1.5. For a weighed portion, divide its grams by nutrition.serving_size.grams only when that serving weight is known. OWS should store consumed servings, nutrient values and their provenance as an immutable log snapshot; future plate corrections must not rewrite past logs. Those snapshots are OWS meal history, not a second food database.

## Search and components

GET /plates?max_kcal=620&min_protein_g=45&sort=protein_per_kcal filters per serving. Stale/incomplete/unknown nutrition is excluded. Sort supports created (default), kcal and minutes ascending, and protein_per_kcal descending. Plates with missing values go last; zero-kcal items have no protein-per-calorie rank. Unsupported sort keys return 400. Filtering still searches only the connected user's private library.

POST a full plate body with kind:"component", one or more ingredients and steps:[] to save a component without cooking steps. All existing required plate fields still apply. kind is plate by default. GET /plates?kind=component or kind=plate filters explicitly; unfiltered results include both for compatibility. Components currently count toward the library cap, with an early-access capacity of 500. Fit-to-target, substitutions, generated pantry meals and composite meal definitions are not shipped in this release.

## Quotas and auth

GET /me now returns token_expires_at (ISO string or null for Firebase sessions), scopes, and quotas containing library_cap and extracts.limit/remaining/resets_at/global_safety_limit. Refresh this before showing quota state. A 200-global/day safeguard and 15/user/day remain the extraction defaults; this is not a partner SLA or an increased spending commitment. POST /extracts includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix seconds), and X-RateLimit-Resource. Quota 429 includes Retry-After in seconds. A global 429 can occur with positive user remaining; read the error message. Polling, food lookup and CRUD do not consume extraction quota. These headers describe extraction allowance, not generic request throttling; they are not emitted on every read.

Manual tokens still last 90 days. [OAuth account linking](https://plateherder.com/oauth-integration.md) is now available for registered clients. Webhooks, ETags and incremental sync remain follow-ups.

### Shakes

`kind` is `plate`, `shake`, or `component` (shown as Foods). Use `kind=shake` on `GET /api/v1/plates` for shakes and smoothies. The same value is accepted on creation, extraction-save overrides (`plate.kind`), and PATCH. Shakes use the same nutrition, servings, ingredients, steps and ownership model as plates; all nutrition filters apply per serving. Omitting `kind` searches all three kinds. A homemade shake is a shake; a packaged drink or individual ingredient is a component. Clients should let the user correct the kind. Category edits alone do not invalidate nutrition.

## Automatic estimation

POST /plates/:id/nutrition requires plates:write. It retains complete source/user/ingredient nutrition when available; otherwise it can generate a rough AI estimate from the saved title, ingredients, quantities and cooking steps. This is separate from USDA arithmetic. Notes and tags are omitted from the provider request.

Responses use method `ai`, confidence `estimated`, disclosed quantity assumptions in `warnings`, and `assumed_servings` if the recipe had no serving count. Original recipe quantities and servings are not overwritten. `complete` here means values are present, not that quantities were observed or accuracy verified. Display the rough-estimate label and assumptions alongside the values. Repeated calls reuse the saved estimate. Recipe basis edits invalidate it; unrelated notes edits preserve it. Writes reject concurrent recipe changes.

Incomplete USDA calculations may return `partial_total` and `partial_per_serving` with `matched_ingredients` and `ingredient_count`. These are known-ingredient subtotals, never complete recipe totals.
