See Library & Recipes and
Import from a URL for what these operations do in
the UI. A recipe’s id is a slug derived from its title (deduplicated
with a numeric suffix on collision), not a UUID.
GET /api/recipes
GETAny session
Lists every recipe in the library — the shared record only, with no
per-profile rating, favorite, or cooked status attached (that state lives
separately; see PATCH /api/recipes/:id below).
Response: { "recipes": RecipeRecord[] }
POST /api/recipes
POSTAny session
Creates a recipe by hand. multipart/form-data:
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | Yes | |
source |
string | No | Author or site name |
sourceUrl |
string | No | |
description |
string | No | |
servings |
number | No | |
prepMinutes |
number | No | |
cookMinutes |
number | No | |
difficulty |
"easy" | "medium" | "hard" |
No | |
cuisine |
string | No | |
tags |
JSON string | No | JSON array of strings |
ingredients |
JSON string | No | JSON array of { quantity?, unit?, name, note? } |
steps |
JSON string | No | JSON array of { id?, text } |
image |
file | No | Hero photo; rejected with 413 past the server’s upload size limit — see Settings |
step-image-<stepId> |
file | No | One field per step that has a photo, keyed by that step’s id in the steps JSON |
Response: 201 with { "recipe": RecipeRecord }, or 400 if
title is missing, 413 if a photo is too large.
POST /api/recipes/import
POSTAny session
Imports a recipe from a URL — see Import from a URL for how the scraper works.
Request body: { "url": string }
Response: 201 with { "recipe": RecipeRecord }, 400 if url is
missing or not http/https, or 422 if the page has no scrapeable recipe.
POST /api/recipes/:id/edit
POSTAny session
Updates an existing recipe. Same fields as POST /api/recipes above.
Steps whose photo isn’t being replaced should echo back their current
hasImage/imageExt in the steps JSON — the server diffs against the
existing record and deletes any now-orphaned step photos.
Response: { "recipe": RecipeRecord }, or 404 if the recipe
doesn’t exist, 400 if title is missing, 413 if a photo is too
large.
PATCH /api/recipes/:id
PATCHAny session
Updates the active profile’s state for one recipe — rating, favorite, and cooked status all live per-profile, not on the shared recipe record.
Request body — all fields optional:
| Field | Type | Notes |
|---|---|---|
rating |
integer 0–5 | 0 clears the rating |
favorite |
boolean | |
cooked |
boolean | Setting it true also stamps lastCookedAt |
Response: { "recipe": Recipe } — the recipe merged with the active
profile’s state.
DELETE /api/recipes/:id
DELETEAdmin only
Deletes the recipe’s files from storage, removes it from the index, and clears every profile’s state for it.
Response: { "ok": true }
GET /api/recipes/:id/activity
GETAny session
Lists other profiles (excluding the active one) that have cooked, rated, or favorited this recipe — powers the “Cooking Activity” panel on a shared install.
Response: { "activity": [{ "profileId": string, "name": string, "color": string, "cooked": boolean, "rating"?: number }] }