Skip to main content

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​

SetupBase URL
Docker Composehttp://localhost:8787/api/v1
Source checkouthttp://localhost:4787/api/v1
Cloudflare Tunnelhttps://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:

  1. If an Authorization header is present, it must be a valid bearer token. An invalid token returns 401, even if the request also has a session cookie.
  2. With PANTRYPAL_PASSWORD configured, a valid browser session cookie authenticates the request. Unsafe methods must also pass the same-origin check.
  3. Without a password, PANTRYPAL_TRUST_NETWORK=1 lets a request without an authorization header act as the household.
  4. 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.

EndpointBehavior
GET /auth/session{"enabled": boolean, "authenticated": boolean}. Cache-Control is no-store.
POST /auth/loginJSON 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/logout204; 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.

PathTasks
/household, /household/membersRead and update settings and members
/ingredientsFind, create, update, archive, alias, and merge ingredients
/unit-conversions, /units/convertManage conversions and convert amounts
/pantryRead stock, set amounts, record changes, and check availability
/recipesManage recipes and step ingredient links, scale servings, check rules, and set status
/rulesManage dietary rules and check ingredient lists
/plansManage week plans, meal slots, dishes, swaps, cooks, and prep tasks
/cook-eventsCreate, complete, reopen, and undo cooks
/prep-tasksUpdate or complete prep tasks
/shopping-listsGenerate 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 true or false.
  • 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 create and update bodies accept steps[].links. Each link has an ingredientIndex and an optional qty:

  • ingredientIndex is the zero-based position in the same payload's ingredients array. When a patch sends only steps, it uses the stored line order.
  • qty uses the linked line's unit. null or omitted means the whole line.
  • The server rejects an index that does not point to a line of the same recipe.
  • If ingredients is replaced without steps, 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.

CodeHTTP statusMeaning
validation400, 413Invalid body, path, or query; login body over 4 KiB
unauthorized401Missing, invalid, or revoked token; missing or invalid browser session; wrong password
forbidden403Cross-origin login, logout, or unsafe cookie request
not_found404No matching record in this household, or browser login is not configured
conflict409A conflict with current data
unsupported_media_type415Login body is not application/json
unprocessable422Valid input that cannot be applied
rate_limited429Too many failed logins; retry after Retry-After seconds
unavailable503A required resource is not ready, such as the household
internal500Unexpected 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