Skip to content

Recipes

Add, import, list, edit, delete recipes, and read cooking activity.

Updated View as Markdown

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
videoUrl string No Must be http/https; a YouTube link embeds in the recipe view, anything else shows as a “Watch the video” link
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?, isGroup? }
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

Array order is display order for both ingredients and steps — there’s no separate ordering field, so reordering means resending the array in the new order. An ingredients entry with isGroup: true is a section header, not an ingredient: name holds the header text and quantity/unit/note are ignored (the server clears them even if sent).

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. A YouTube URL is handled separately: it always sets videoUrl, title, source (channel name), and the cover photo from YouTube’s oEmbed endpoint, and best-effort parses ingredients and steps out of the video description when it finds any.

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 (YouTube URLs only 422 when the video itself can’t be found).

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.

POST /api/recipes/:id/duplicate

POSTAny session

Duplicates a recipe — copies its metadata and photos (hero and step images) under a new id, derived the same way as a title’s (see above, deduplicated with a numeric suffix if needed). The copy’s title gets a “ (Copy)“ suffix and addedAt is reset to now; everything else, including sourceUrl, carries over unchanged. The copy has no per-profile state (rating, favorite, cooked) — PATCH /api/recipes/:id below starts it fresh, same as any new recipe.

Response: 201 with { "recipe": RecipeRecord }, or 404 if the recipe doesn’t exist.

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 }] }

Navigation

Type to search…

↑↓ navigate↵ selectEsc close