Flambe docs
Guides

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, 1600

Any 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, then POST /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.

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.

On this page