Recipes
The recipe model, and how to create, update and organize a library.
A recipe is the central object. Imports produce them; you can also create them directly.
The model
Only title is required. Everything else is optional, which matters because
extraction from a photograph does not always recover every field.
{
"id": "9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
"title": "Brown Butter Chocolate Chip Cookies",
"subtitle": "Makes two dozen",
"tags": ["dessert", "cookies"],
"prep_time": 20,
"cook_time": 12,
"servings": 24,
"difficulty": "easy",
"rating": 5,
"ingredients": [
{ "name": "all-purpose flour", "quantity": 2.25, "unit": "cups" },
{ "name": "unsalted butter", "quantity": 1, "unit": "cup", "preparation": "browned" },
{ "name": "flaky salt", "isOptional": true }
],
"instructions": ["Brown the butter and let it cool.", "Cream with both sugars."],
"components": [],
"notes": "",
"media": []
}prep_time and cook_time are minutes, as integers.
Ingredients are parsed, not strings
Each ingredient is an object, so 2 ½ cups sifted flour arrives as its parts
rather than a line of text you have to re-parse:
{ "name": "flour", "quantity": 2.5, "unit": "cups", "preparation": "sifted" }Fractions are normalized to decimals. isOptional reflects the source marking
the ingredient optional.
Components are sub-recipes
A recipe with a sauce, a dough and a streusel has three components, each with
its own ingredients and instructions. This is what stops a multi-part recipe
from flattening into one ambiguous list.
When components is non-empty, top-level ingredients holds only the
ingredients belonging to no component. To show every ingredient, concatenate
both.
const allIngredients = [
...recipe.ingredients,
...recipe.components.flatMap((c) => c.ingredients),
];Array fields are always arrays
tags, ingredients, components, instructions and media are normalized
server-side and always come back as arrays — never null, never a
JSON-encoded string. notes is likewise always a string, possibly empty. You
do not need defensive coercion.
Creating
curl -X POST https://api.flambe.dev/api/recipes \
-H "Authorization: Bearer $FLAMBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "Weeknight carbonara", "servings": 2 }'Updating
PUT /api/recipes/{id} replaces only the fields you send — omitted fields are
left alone. Despite being a PUT, it behaves as a partial update, so you can
rate a recipe without resending it:
curl -X PUT "https://api.flambe.dev/api/recipes/$RECIPE_ID" \
-H "Authorization: Bearer $FLAMBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "rating": 5, "notes": "Chilled the dough overnight — better." }'Sending an array field does replace it wholesale. To add one tag, send the full new list.
Deleting
DELETE /api/recipes/{id} is permanent. It also records a revocation, so
clients syncing incrementally learn to drop their copy — see
Syncing.
Listing
curl "https://api.flambe.dev/api/recipes?limit=24" \
-H "Authorization: Bearer $FLAMBE_TOKEN"Returns the caller's library plus anything shared with them through a
household. limit defaults to 24 and caps at 100;
page with cursor.
Nutrition
curl -X POST "https://api.flambe.dev/api/recipes/$RECIPE_ID/nutrition" \
-H "Authorization: Bearer $FLAMBE_TOKEN"Estimates per-serving nutrition by matching ingredients against USDA FoodData Central and Open Food Facts. Two things to know:
- The figures are estimates. Ingredients that cannot be matched are skipped rather than guessed at, so a recipe with unusual ingredients under-reports.
- The result is cached against
ingredients_hash. If that hash no longer matches the recipe's ingredients, the stored nutrition is stale — recompute.
Copying someone else's recipe
POST /api/recipes/{id}/save-copy copies a recipe you can see — typically one
shared through a household — into your own library as an independent record.
Later edits to the original do not propagate.
Images
Recipe media entries carry a CDN URL on cdn.flambe.dev. Width variants are
available by appending @{width}w.webp, for these widths only:
320, 480, 640, 800, 960, 1200, 1600Any other width falls back to the original file, so do not invent sizes.
<img
src="https://cdn.flambe.dev/files/abc@640w.webp"
srcset="
https://cdn.flambe.dev/files/abc@320w.webp 320w,
https://cdn.flambe.dev/files/abc@640w.webp 640w
"
sizes="(max-width: 640px) 100vw, 50vw"
/>Each entry also carries blur_hash for a placeholder while the image loads.
Organizing
- Collections group recipes by name —
POST /api/collections, thenPOST /api/collections/{id}/recipes. Deleting a collection does not delete its recipes. - Tags live on the recipe itself.
- Sources record where a recipe came from. See the Sources section of the reference.
Semantic search
Recipes are indexed as embeddings. To search over recipes you have synced
locally, embed the query with POST /api/ai/embed-query — using the same
model that built the index — and compare vectors yourself:
curl -X POST https://api.flambe.dev/api/ai/embed-query \
-H "Authorization: Bearer $FLAMBE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "query": "something warm with lentils" }'Embedding the query with a different model produces distances that are not comparable to the index, which is the whole reason this endpoint exists.