# PlateHerder integration handoff for Claude / OWS

Owner instruction: implement OWS as a client of the already deployed PlateHerder engine. Do not copy its database or put cooking extraction inside OWS. This document describes API v1, not a completed OWS integration. Google verification is pending; its checklist is preserved in the PlateHerder repo at docs/GOOGLE-VERIFICATION.md.

## Connection

- Production base: https://plateherder.com/api/v1
- Machine-readable contract: https://plateherder.com/openapi.json
- Public overview: https://plateherder.com/developers
- Logos, connection-button guidance and optional attribution badges: https://plateherder.com/brand
- Authentication: `Authorization: Bearer ph_...`
- Preferred connection: [OAuth authorization code + PKCE](https://plateherder.com/oauth-integration.md). Register the OWS client and exact callback URLs first. The user signs into PlateHerder, approves permissions and returns to OWS; no copied token. The linked guide covers state validation, code exchange, token rotation and disconnect.
- OWS stores each user's token encrypted on its server and associates it with that authenticated OWS user. Never use one owner's token for everyone. Call `GET /me` to validate the connection and store the returned `id` as `plateherder_user_id`. A token with `plates:read` is required for this check. If reconnecting returns a different ID, explicitly confirm the account switch and clear old display caches/pins as appropriate.
- Manual 90-day tokens remain available for scripts/beta connections through Account. OAuth access tokens last one hour, refresh tokens rotate with a 30-day idle expiry and a one-year connection maximum. Show Reconnect after revocation or invalid_grant. Disconnect through the revocation endpoint and remove local credentials; users can also revoke access in PlateHerder Connected apps.
- Scopes: `plates:read` for identity/library/detail; `plates:write` for save/edit/delete; `extract:create` for starting and polling jobs. Normal read access includes personal plate notes and tags: disclose that when connecting OWS, and keep that data private to the user.
- Use OWS server routes as the proxy. PlateHerder does not provide a cross-origin browser API or accept `X-User-Id` for impersonation. Do not pass Firebase admin credentials or Google tokens to OWS.

## First OWS surface

1. A Meals tab with Connect PlateHerder or the connected user's plate library.
2. Search: `GET /plates?q=chicken&ingredient=rice&max_minutes=30&limit=25`. Follow `next_cursor` until null. Filters are optional; `diet_tag` is singular and an exact source-label match. Unknown total times are excluded when max_minutes is supplied. Do not infer nutrition from tags.
3. Detail: `GET /plates/{id}`. Display ingredients, steps, warnings, source link and creator. Use the official YouTube embed for video_id; never download media. Nullable values mean unknown. Timestamp status describes provenance, not guaranteed accuracy.
4. Save from link/text: `POST /extracts` with exactly `{ "url": "https://..." }` or `{ "text": "..." }`. HTTP 202 returns the job ID. Poll `GET /extracts/{id}` every 2-3 seconds with the same user's token and show stage/progress. Stop at `ready`, `needs_review` or `failed`. A failed job returns HTTP 200 with a failure status. Pause polling when hidden; preserve the job ID to resume. Jobs expire after 24 hours.
5. On ready/needs_review, show the draft and allow explicit Save. `POST /plates` with `{ "extract_id": "..." }`. needs_review is usable but requires human checking; do not present it as verified. Repeating Save for one extract returns the same plate. Starting extraction again creates a new job and consumes quota, so do not blindly retry POST after a timeout.
6. Edit via `PATCH /plates/{id}`. Arrays replace entire arrays and writes currently use last-write-wins semantics. Avoid background autosave over a newer user edit. Delete via `DELETE /plates/{id}` (204, no JSON body). Deleting a plate does not remove independent household or shopping snapshots.

## Data boundaries and known limitations

- PlateHerder is the source of truth. OWS may retain a plate ID, user mapping and minimal display cache, not duplicate ingredient/step tables. Fetch fresh on opening a plate; clear cached content on 404, account disconnect/switch or deletion. A longer-lived offline cache needs an explicit retention/deletion design before release.
- Responses include YouTube API source metadata when applicable. Private playlist membership, liked-video catalogs and Google access tokens are not part of this API. Preserve attribution. Do not use this API to make a public recipe corpus.
- Google data-use/metadata-retention/downstream-sharing review is still pending. OWS integration testing helps establish the actual flow; it does not itself constitute Google approval.
- Nutrition v1.1 is available: see https://plateherder.com/nutrition-api.md for calculation, matching, user correction and stale-value rules. Unknown numbers remain null. total_minutes is source-provided and may differ from prep_minutes + cook_minutes.
- UUID plate/job IDs; user IDs are opaque strings. JSON success bodies are not wrapped in a data property. Ingredient and step subresources return `{ingredients: [...]}` and `{steps: [...]}`.
- Defaults: 500 saved items per early-access account, 15 extractions per signed-in user/day, 200 globally/day; configurable. GET /me reports the configured library cap, token expiry and user extraction allowance. The global cap is a service-wide spending safeguard, not 200 per integration. POST /extracts returns X-RateLimit-Limit/Remaining/Reset and Retry-After on quota 429. Polling and CRUD do not consume extraction quota. A partner allocation/billing tier is not implemented; do not plan a broad launch on the default global budget. Library scanning is bounded to 500 records.
- Errors: `{ "error": { "message": "..." } }`. 400 invalid fields, 401 missing/expired Firebase session, 403 invalid/expired/revoked/underscoped account token, 404 missing/non-owned/expired resource, 409 capacity/not-ready/claim conflict, 413 oversized body, 429 quota, 5xx service failure. Never infer ownership from a 404.
- Treat new response fields as additive. Within v1 keep existing field meanings. Parse 204 without attempting JSON. Use finite request timeouts and no-store responses on the OWS proxy.
- Do not call internal `/api/discover`, `/api/youtube-lists`, `/api/household` or `/api/shopping` with integration tokens. OAuth linking and those integration APIs are not included in this release.

## Acceptance checks for Claude

Connect two separate test users and verify that neither can read, edit or delete the other's plates. Check read-only token write rejection, revocation/expiry handling, paging/search, link -> job -> explicit save -> detail, needs_review and failure displays, edit/delete, null times and source attribution. Confirm tokens never appear in browser responses, logs or client bundles. Do not use a real user's private library as destructive test data.

The PlateHerder repo includes `scripts/ows-smoke.mjs`, a dependency-free HTTP client. Set PLATEHERDER_TOKEN securely in the process environment and run `node scripts/ows-smoke.mjs`. Default mode tests identity, manual create/read/search/patch/delete and cleans up its own plate. Add `--extract` to test a real async extraction (consumes quota), optionally with PLATEHERDER_SOURCE_URL. No OWS or Firebase database access is needed by that script.
# Library cards and foods

For lightweight card lists, add `format=summary` to `GET /api/v1/plates`. Use `kind=plate` for dishes or `kind=component` for saved foods. Summary records include `source` (including available YouTube thumbnail), `nutrition`, `total_minutes`, `ingredient_count` and ordinary card metadata; fetch the plate detail endpoint for ingredients, steps and notes. Treat `next_cursor` as opaque and reset it whenever filters or sort change. The default response without `format=summary` remains unchanged.

Account linking should not populate a user's library. Searching `/foods` is read-only; create a component only when the user explicitly saves a food portion. Keep those components separate from dishes in the UI.


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

Saved library entries may have `extract_status: pending | processing | failed | ready | needs_review`. PlateHerder can save a source before preparation finishes. Show pending/processing entries as preparing and failed entries with `extract_error`; do not offer meal logging until usable nutrition is available. These entries persist independently of the browser.


### Multiple recipes from one video

A saved video can now be a recipe group. List summaries include `recipe_count`; detail responses include a `recipes` array (2?12 separate recipes). Each child has its own title, kind, ingredients, steps, servings, warnings and nutrition when available, and shares the parent source. The group has no combined nutrition, servings or ingredient list. Do not log the group as one meal.

To save one child as an independently editable plate, POST `/api/v1/plates` with `{"collection_id":"PARENT_PLATE_ID","recipe_index":0}` (zero-based). Requires `plates:write` and an owned parent. Repeated saves of the same child return the same saved plate. Use the returned plate id for logging, planning and shopping. Separately saved recipes are copies; subsequent edits do not change the group or other copies. Existing failed multi-recipe imports can be retried.

### Saved foods

Components can include `food_name` and `brand` for grocery favorites. Display `food_name` when present. Nutrition serving amounts remain separate from purchase quantities. Foods created without a measured serving have unknown nutrition; do not infer loggable macros from a shopping quantity.

### Library organization

Saved plates support optional `favorite`, `collections` (up to 20 names), and `food_category` fields through PATCH and in list summaries. These organize a saved item without copying it. Summary listings accept up to 500 items, matching the early-access library capacity; ordinary detail listings retain their existing limit. Quick meal filters require a known time of 30 minutes or less; high protein requires complete, non-stale nutrition of at least 25 g protein per serving.
