REST API
The JSON API is at /api/v1. It uses the same services as MCP.
The running server's /openapi.json describes every endpoint, input, and response.
Connect
| Setup | Base URL |
|---|---|
| Docker Compose | http://localhost:8787/api/v1 |
| Source checkout | http://localhost:4787/api/v1 |
| Cloudflare Tunnel | https://pantry.example.com/api/v1 |
Send a bearer token:
curl -sS http://localhost:8787/api/v1/household \
-H "Authorization: Bearer $PANTRYPAL_TOKEN"
See CLI commands to create a token.
Authentication
/api/v1 checks credentials in this order:
- If an
Authorizationheader is present, it must be a valid bearer token. An invalid token returns401, even if the request also has a session cookie. - With
PANTRYPAL_PASSWORDconfigured, a valid browser session cookie authenticates the request. Unsafe methods must also pass the same-origin check. - Without a password,
PANTRYPAL_TRUST_NETWORK=1lets a request without an authorization header act as the household. - Otherwise the request returns
401.
Password mode disables anonymous network trust. /mcp always needs a bearer
token; it never accepts a session cookie or network trust. See
Access and security.
Browser session endpoints
These paths are outside /api/v1. They are public, but login and logout require
the app's own origin.
| Endpoint | Behavior |
|---|---|
GET /auth/session | {"enabled": boolean, "authenticated": boolean}. Cache-Control is no-store. |
POST /auth/login | JSON body {"password": "…"}. 204 and a session cookie on success; 401 on a wrong password; 403 on a cross-origin request; 404 when no password is configured; 429 with Retry-After when throttled. |
POST /auth/logout | 204; clears the session cookie. |
The cookie is __Host-pantrypal_session, valid for 30 days, HttpOnly,
Secure, SameSite=Lax, and scoped to /. Rotating PANTRYPAL_PASSWORD
invalidates every session. Login accepts JSON only (415 otherwise), limits the
body to 4 KiB (413), and allows 10 failed attempts per minute per socket peer
plus 40 per minute in total; behind a proxy, clients may share the per-peer
limit. Safe methods need no origin check; unsafe cookie-authenticated requests
need an Origin equal to Host, over HTTPS except for loopback development.
X-Forwarded-Host is ignored and Sec-Fetch-Site: cross-site is rejected.
A valid browser session when no household exists returns 503 with code
unavailable. Create one with household init.
GET /health and GET /openapi.json are public. /api/v1/health uses API auth.
Endpoint groups
Paths below are relative to /api/v1.
| Path | Tasks |
|---|---|
/household, /household/members | Read and update settings and members |
/ingredients | Find, create, update, archive, alias, and merge ingredients |
/unit-conversions, /units/convert | Manage conversions and convert amounts |
/pantry | Read stock, set amounts, record changes, and check availability |
/recipes | Manage recipes and step ingredient links, scale servings, check rules, and set status |
/rules | Manage dietary rules and check ingredient lists |
/plans | Manage week plans, meal slots, dishes, swaps, cooks, and prep tasks |
/cook-events | Create, complete, reopen, and undo cooks |
/prep-tasks | Update or complete prep tasks |
/shopping-lists | Generate lists, check items, and record purchases |
Conventions
- Send JSON bodies with
Content-Type: application/json. - Fields use camelCase. Dates use
YYYY-MM-DD; timestamps use ISO 8601. - Reuse returned IDs. Do not construct them.
- Boolean query values are
trueorfalse. - An unknown stock amount is not zero. Check its status as well as its quantity.
- Use server calculations for conversions, servings, demand, and shopping amounts.
Most lists return {"items": [...]}. Recipes also return total, limit, and
offset. Pantry movements use limit and before for older entries. Shopping
list detail returns list, items, groups, counts, and runningLow.
Recipe step links
Recipe create and update bodies accept steps[].links. Each link has an
ingredientIndex and an optional qty:
ingredientIndexis the zero-based position in the same payload'singredientsarray. When a patch sends onlysteps, it uses the stored line order.qtyuses the linked line's unit.nullor omitted means the whole line.- The server rejects an index that does not point to a line of the same recipe.
- If
ingredientsis replaced withoutsteps, the links are removed.
Recipe read returns the links on each step as links[], with ingredientName,
qty, unit, prepNote, and position. The recipe also returns
cookEventCount; DELETE /recipes/{idOrSlug} conflicts when it is greater than
zero.
Write and delete responses vary. Use the response schema for the endpoint.
Errors
{"error":{"code":"not_found","message":"Recipe not found","details":{}}}
details is optional. Use the code, not the message text, in client logic.
| Code | HTTP status | Meaning |
|---|---|---|
validation | 400, 413 | Invalid body, path, or query; login body over 4 KiB |
unauthorized | 401 | Missing, invalid, or revoked token; missing or invalid browser session; wrong password |
forbidden | 403 | Cross-origin login, logout, or unsafe cookie request |
not_found | 404 | No matching record in this household, or browser login is not configured |
conflict | 409 | A conflict with current data |
unsupported_media_type | 415 | Login body is not application/json |
unprocessable | 422 | Valid input that cannot be applied |
rate_limited | 429 | Too many failed logins; retry after Retry-After seconds |
unavailable | 503 | A required resource is not ready, such as the household |
internal | 500 | Unexpected server error |
OpenAPI
Download the schema from a running server:
curl -sS http://localhost:8787/openapi.json -o pantrypal-openapi.json
To update the committed schema and web client types from source:
pnpm openapi
This writes web/openapi.json and
web/src/api/schema.d.ts.
Do not edit those files by hand.
The web client uses openapi-fetch. Its base URL is /api/v1; its generated
paths omit that prefix. The OpenAPI document keeps the full paths and has no
servers entry. Set the base URL when you generate another client.
Next: MCP tools · Architecture