---
name: mealie
description: Adding and editing recipes in the family Mealie instance, including the non-obvious ingredient-parsing requirements
---
- URL: https://meals.vino.network (Mealie v3.18.x, OIDC via Authentik)
- Hosted in sjc001 cluster, `mealie` namespace
- Family group/household: `Home` / `Family`
- API token: stored in `code/git/code.fritzlab.net/fritzlab/agent/.env` as
`MEALIE_TOKEN` (paired with `MEALIE_URL=https://meals.vino.network`).
Regenerate at https://meals.vino.network/user/profile/api-tokens.
- All endpoints take `Authorization: Bearer $MEALIE_TOKEN`.
- **Every recipe must carry exactly one meal-type tag**: `Breakfast`, `Lunch`, `Dinner`,
or `Dessert`. When importing or editing a recipe, add the appropriate one; preserve
any other tags already on the recipe. If meal type is ambiguous (e.g. banana bread →
breakfast or dessert), ask the user — don't pick silently.
- Meal-type tag IDs in the family Mealie:
- `Breakfast` = `38fb71bf-a89d-45ee-9d21-0b1e5f716569`
- `Lunch` = `5f0ce972-2243-4ef6-b3c7-ad24f8561e84`
- `Dinner` = `bc992ba9-80ce-4407-8d1e-e69a7f659ebb`
- `Dessert` = `e7adc953-3f23-4032-804b-ada18b3a2da9`
- To append a tag without clobbering: GET the recipe, take `tags`, append the new
`{id, name, slug}` object, PATCH back with `{"name": , "tags": [...]}`.
GET /api/users/self — confirms the token works and returns group/household.
Mealie scrapes recipe sites that publish schema.org Recipe JSON-LD.
POST /api/recipes/create/html-or-json with `{"url":"https://..."}` — returns the new slug.
Falls back to scraping HTML if no JSON-LD. PDFs and image URLs do NOT work here.
Two-step: create then patch. Mealie has no single endpoint that takes a full recipe at once.
1. POST /api/recipes body `{"name": "..."}`
Returns the slug as a **raw JSON string** (e.g. `"my-recipe"`), not an object.
If a recipe with the same name exists, Mealie auto-suffixes (`-1`, `-2`). To avoid
duplicates, GET /api/recipes first and match by name before posting.
2. PATCH /api/recipes/{slug} with the full Recipe-Input body:
- `name`, `description`, `prepTime`, `cookTime`, `totalTime` (free strings)
- `recipeYield` (string), `recipeServings` (number), `recipeYieldQuantity` (number)
- `recipeIngredient: [...]` — see ingredient-parsing below
- `recipeInstructions: [{text, ingredientReferences: []}, ...]`
- `tags: [{id, name, slug}, ...]`, `recipeCategory: [{id, name, slug}, ...]`
- `notes: [{title, text}, ...]`
- `orgURL: "..."` — source link
- DO NOT include `slug` in the PATCH body; it triggers a rename attempt that conflicts
with the current slug and returns 400 "Recipe already exists".
Same as step 2 above: PATCH /api/recipes/{slug} with the fields to change. Other fields
are preserved. To replace the entire ingredient list, send the full new `recipeIngredient`
array. To clear a list, send `[]`.
DELETE /api/recipes/{slug} removes the recipe.
Mealie has an NLP parser (also `brute` and `openai` variants) that splits raw strings like
`"1½ tablespoons mayo"` into structured `{quantity, unit, food, note}` objects. The unit
and food are objects with their own `id` referencing rows in `/api/units` and `/api/foods`.
POST /api/parser/ingredients
body: `{"parser":"nlp", "ingredients":["1½ tablespoons mayo", "4 ears fresh corn, husked", ...]}`
returns one ParsedIngredient per input.
Parser gotchas that bite every time:
1. **Unit / food objects with `id: null` cannot be PATCHed onto a recipe** — server raises
`ValueError: Expected 'id' to be provided for unit/food`. The parser returns `id: null`
whenever the unit/food name isn't already in the database. Workflow: after parsing,
resolve each name against `/api/units` and `/api/foods` (auto-creating missing ones via
POST), then set `ing.unit.id` and `ing.food.id` before PATCHing the recipe.
2. **Seed the locale before parsing** or the parser fuzzy-matches against whatever you
created previously. Concrete bite: with only `tablespoon` in the unit table, every
"1 teaspoon" parses to `unit_id = ` because the Levenshtein distance
is 1. Pre-seed once per instance:
- POST /api/groups/seeders/units `{"locale":"en-US"}`
- POST /api/groups/seeders/foods `{"locale":"en-US"}`
These populate the standard US units (cup, teaspoon, tablespoon, ounce, etc.) and
common foods. After seeding the parser stops conflating teaspoon/tablespoon. Already
done on the family Mealie 2026-05-22.
3. **Sections in an ingredient list** (e.g. "Cookie dough" vs "Buttercream" in the same
recipe): set `title` on the FIRST ingredient of each section. Convention used in the
family import script: prefix the raw input with `[Section Name]`, the helper strips
the bracket and promotes it to `title` on the first ingredient of that group.
4. **Instructions also have a hidden required field**: each step object must include
`ingredientReferences: []`. The OpenAPI schema says it defaults to `[]`, but PATCH's
`model_dump(exclude_unset=True)` excludes it and the SQLAlchemy model then raises
`RecipeInstruction.__init__() missing 1 required positional argument: 'ingredient_references'`.
Always send `ingredientReferences: []` explicitly.
RecipeTag and RecipeCategory objects in a PATCH body need **all three** of `id`, `name`,
`slug`. Sending just name+slug produces a misleading 400 "Recipe already exists" (the
real cause is a SQL integrity error logged server-side as `SQL Integrity Error on recipe
controller action`).
Create with POST /api/organizers/tags or POST /api/organizers/categories — body
`{"name":"..."}` — response contains the `id` and `slug` to reuse.
Existing IDs in the family Mealie:
- Tag `Mexican Street Eats Class` = `bb6841c5-3b6d-4569-b707-3d14bea83d7b`
- Category `Mexican` = `6ee64be6-83ce-4d6e-926c-62edfb791a41`
- `400 "Recipe already exists"` on PATCH → almost always means tag/category/food/unit
is missing an `id`. Check the server logs (`kubectl --context sjc001 -n mealie logs
-l app=mealie`) for `SQL Integrity Error` to confirm.
- `500 TypeError` on PATCH → missing `ingredientReferences: []` on an instruction.
- `500 ValueError: Expected 'id' to be provided for unit` → parsed ingredient still has
`unit.id: null` or `food.id: null`; resolve via /api/units, /api/foods first.
- `500` on POST /api/foods or /api/units when the name already exists → list and dedupe
before creating, or catch the UniqueViolation.
A working ingest script for the Mexican Street Eats PDF lives at
`/tmp/add_mealie_recipes.py` during the 2026-05-22 session. Pattern to reuse:
1. Build food + unit name→id caches once (GET /api/foods, /api/units, paginate).
2. For each raw ingredient line, POST to /api/parser/ingredients.
3. For each parsed result with `unit.id: null` or `food.id: null`, look up by lowercased
name in the cache; POST to create if missing; update cache.
4. For sectioned recipes, strip `[Section]` prefix and set `title` on first item.
5. Build PATCH body with `recipeIngredient`, `recipeInstructions` (each with
`ingredientReferences: []`), `tags` and `recipeCategory` (with ids).
6. POST to /api/recipes for the name, then PATCH /api/recipes/{slug} with the body.