{"openapi":"3.1.0","info":{"title":"Meal Coordinator","version":"1.0.0","description":"Shared store for a household's weekly meal plans and its library of favourite ('winner') recipes. Meal plan entries are self-contained snapshots, so scaling a week or swapping an ingredient never edits the library recipe. It also holds a queue of suggested recipe links the household wants tried: read it when planning a week, and mark each one used so the household can vote on it after eating.\n\nEvery endpoint is scoped to the account your API key belongs to. You see that account's recipes, plans, suggestions and preferences and no others, and anything you create is filed under it. Ids from one account do not resolve in another: a recipe belonging to someone else reads as 404, not 403."},"servers":[{"url":"https://www.meal-agent.com"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A key from the Account page of the web UI. It identifies the account to act for, so keys are not interchangeable between households."}},"schemas":{"RecipeInput":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"description":{"type":["string","null"],"description":"One or two sentences on the dish itself -- what it is, how it tastes, why you would cook it. Shown under the title. Write one for every recipe."},"cuisine":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"servings":{"type":["integer","null"],"description":"Descriptive label only. Nothing is scaled from it."},"prep_minutes":{"type":["integer","null"]},"cook_minutes":{"type":["integer","null"]},"body":{"type":"string","description":"The recipe as markdown -- ingredients, method, notes. Stored verbatim and rendered as markdown on the web and printed pages. Use '## Ingredients' and '## Method' headings with lists."},"source":{"type":["string","null"]},"notes":{"type":["string","null"]},"status":{"type":"string","enum":["winner","candidate","archived"],"default":"winner"},"rating":{"type":["integer","null"],"minimum":1,"maximum":5}}},"EntryInput":{"type":"object","required":["scheduled_on","meal_type","title"],"properties":{"recipe_id":{"type":["string","null"],"format":"uuid","description":"Set this when the meal drew on a library recipe. Used only to count how often a winner gets cooked, so future planning can avoid repeats. Never used to render the meal."},"scheduled_on":{"type":"string","format":"date","description":"YYYY-MM-DD"},"meal_type":{"type":"string","enum":["breakfast","lunch","dinner","snack"]},"sort_order":{"type":"integer","default":0},"title":{"type":"string"},"description":{"type":["string","null"],"description":"One or two sentences on the dish itself, so the day reads at a glance -- 'Ground beef and black beans over rice, baked under melted cheddar.' Never the week's plan or who is eating; that is the plan's household_note. Write one for every entry."},"servings":{"type":["integer","null"],"description":"How many this day's version feeds. A label -- nothing is scaled from it."},"prep_minutes":{"type":["integer","null"]},"cook_minutes":{"type":["integer","null"]},"body":{"type":"string","description":"This day's version as markdown, with amounts already written for however many are eating and any swaps applied. Stored verbatim -- you do the scaling, this API does none."},"notes":{"type":["string","null"],"description":"What is different this week, e.g. 'Doubled - kids here. Tofu instead of chicken.'"}}}}},"paths":{"/api/preferences":{"get":{"operationId":"getPreferences","summary":"Read standing household preferences","description":"Markdown covering diet, likes, dislikes, allergies, household size and routines. Read this before planning a week.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Preferences markdown"}}},"put":{"operationId":"updatePreferences","summary":"Replace standing household preferences","description":"Full replacement, not a merge. Read the current value first, edit it, then write the whole document back.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content"],"properties":{"content":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated preferences"}}}},"/api/recipes":{"get":{"operationId":"listRecipes","summary":"List or search the winners library","description":"Returns saved recipes with times_planned and last_planned_on so you can favour under-used ones and avoid repeating last week.","security":[{"bearerAuth":[]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Free-text match on title, description, cuisine."},{"name":"tag","in":"query","schema":{"type":"string"}},{"name":"cuisine","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["winner","candidate","archived","all"]},"description":"Defaults to 'winner'."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"random","in":"query","schema":{"type":"boolean"},"description":"Shuffle before limiting. Use when asking for inspiration."}],"responses":{"200":{"description":"Matching recipes"}}},"post":{"operationId":"createRecipe","summary":"Save a recipe to the winners library","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeInput"}}}},"responses":{"201":{"description":"Created recipe"}}}},"/api/recipes/{id}":{"get":{"operationId":"getRecipe","summary":"Get one library recipe","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Recipe"},"404":{"description":"Not found"}}},"patch":{"operationId":"updateRecipe","summary":"Edit a library recipe","description":"Only for permanent corrections to the canonical recipe. For a one-week change, edit the plan entry instead.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecipeInput"}}}},"responses":{"200":{"description":"Updated recipe"}}},"delete":{"operationId":"archiveRecipe","summary":"Archive a library recipe","description":"Archives by default -- prefer that. hard=true deletes permanently and should only be used when explicitly asked; either way past plans keep their snapshots and the recipe stays recoverable via /restore.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"hard","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Archived or deleted"}}}},"/api/recipes/{id}/versions":{"get":{"operationId":"listRecipeVersions","summary":"Every prior state of a recipe","description":"An automatic undo log, newest first. Survives permanent deletion of the recipe, so check here before concluding a winner is lost.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Versions, newest first"}}}},"/api/recipes/{id}/restore":{"post":{"operationId":"restoreRecipe","summary":"Undo an edit or an accidental delete","description":"Puts a recipe back from its history. Defaults to the most recent snapshot; pass version_id to go further back.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"version_id":{"type":"string","format":"uuid","description":"From listRecipeVersions. Omit for the most recent snapshot."}}}}}},"responses":{"200":{"description":"The restored recipe"}}}},"/api/plans":{"get":{"operationId":"listPlans","summary":"List meal plans, newest first","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["draft","active","archived","all"]}}],"responses":{"200":{"description":"Plans"}}},"post":{"operationId":"createPlan","summary":"Create a meal plan, optionally with its whole week of meals","description":"Preferred way to write a finished week: send the plan and all entries in one call. Entries are snapshots -- give each one a complete markdown body, already written for however many people are eating that day.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","start_date","end_date"],"properties":{"title":{"type":"string"},"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","archived"],"default":"draft"},"household_note":{"type":["string","null"],"description":"Who is eating and when, e.g. 'Kids Mon-Wed (4), just me Thu-Sun (1)'."},"notes":{"type":["string","null"]},"created_by":{"type":["string","null"],"description":"e.g. 'claude' or 'chatgpt'."},"entries":{"type":"array","items":{"$ref":"#/components/schemas/EntryInput"}}}}}}},"responses":{"201":{"description":"Created plan with entries"}}}},"/api/plans/current":{"get":{"operationId":"getCurrentPlan","summary":"Get the plan covering today","description":"Start here when asked 'what's for dinner'. Falls back to the most recent started plan. Returns plan: null if there is none.","security":[{"bearerAuth":[]}],"parameters":[{"name":"date","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Current plan with entries, or null"}}}},"/api/plans/{id}":{"get":{"operationId":"getPlan","summary":"Get a plan with all its entries","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Plan with entries"}}},"patch":{"operationId":"updatePlan","summary":"Edit plan metadata","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated plan"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["draft","active","archived"]},"household_note":{"type":["string","null"]},"notes":{"type":["string","null"]},"created_by":{"type":["string","null"]}}}}}}},"delete":{"operationId":"deletePlan","summary":"Delete a plan and its entries","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/plans/{id}/entries":{"get":{"operationId":"listPlanEntries","summary":"List the meals in a plan","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Entries"}}},"post":{"operationId":"addPlanEntries","summary":"Add meals to an existing plan","description":"Appends by default. Pass replace=true to clear the plan's meals first.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"replace","in":"query","schema":{"type":"boolean"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/EntryInput"}}}}}}},"responses":{"201":{"description":"Plan with entries"}}}},"/api/entries/{id}":{"get":{"operationId":"getEntry","summary":"Get one meal snapshot","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Entry"}}},"patch":{"operationId":"updateEntry","summary":"Change one meal for this week only","description":"The right tool for 'double Tuesday, the kids are here' or 'swap the chicken for tofu'. The library recipe is untouched.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryInput"}}}},"responses":{"200":{"description":"Updated entry"}}},"delete":{"operationId":"deleteEntry","summary":"Remove a meal from a plan","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/suggestions":{"get":{"operationId":"listSuggestions","summary":"Recipe links the household wants tried","description":"Read this when planning a week and work anything waiting into the plan. Defaults to status=new. An empty queue is the normal case -- plan as you otherwise would rather than treating it as a blocker.","security":[{"bearerAuth":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["new","candidate","winner","rejected","all"]},"description":"Defaults to 'new'. 'candidate' is cooked and awaiting the household's verdict; 'rejected' is what they tried and disliked -- never plan those again."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":200}}],"responses":{"200":{"description":"Suggestions, oldest first"}}},"post":{"operationId":"addSuggestion","summary":"Add a recipe link to the queue","description":"For a link the user wants saved for later rather than cooked now. A URL already in the queue returns created=false with the existing row instead of an error, so this also answers 'have we tried this?'.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"title":{"type":["string","null"],"description":"The dish's real name, if you have read the page."},"note":{"type":["string","null"],"description":"Why it is worth trying."},"added_by":{"type":["string","null"],"description":"e.g. 'claude'."}}}}}},"responses":{"200":{"description":"Already suggested; the existing row is returned"},"201":{"description":"Added"}}}},"/api/suggestions/{id}":{"get":{"operationId":"getSuggestion","summary":"Get one suggestion","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Suggestion"}}},"patch":{"operationId":"updateSuggestion","summary":"Set a suggestion's title or note","description":"Title and note only. The lifecycle moves through /used, /verdict and /retry, so a suggestion can never be dropped straight into 'winner' with nothing promoted.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":["string","null"]},"note":{"type":["string","null"]}}}}}},"responses":{"200":{"description":"Updated suggestion"}}},"delete":{"operationId":"deleteSuggestion","summary":"Remove a suggestion","description":"For a mistyped link. One that was tried and turned down should keep its 'rejected' row instead, so pasting it again says so.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"}}}},"/api/suggestions/{id}/used":{"post":{"operationId":"markSuggestionUsed","summary":"Record that a suggestion was put on a plan","description":"Call this after creating the plan, for every suggestion you cooked into it. It moves the suggestion into the candidates pool and puts the check/X question in front of the household. Pass entry_id: a winning verdict promotes that snapshot into the library, so without it a check has nothing to save.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"entry_id":{"type":"string","format":"uuid","description":"The plan entry you wrote from this link."},"plan_id":{"type":"string","format":"uuid"},"planned_on":{"type":"string","format":"date"},"title":{"type":"string","description":"The dish's real name, now that you have read the page."}}}}}},"responses":{"200":{"description":"Suggestion, now a candidate"},"409":{"description":"Already decided; retry it first"}}}},"/api/suggestions/{id}/verdict":{"post":{"operationId":"recordSuggestionVerdict","summary":"Record the check or the X","description":"Normally clicked by the household in the web UI after eating -- do not call it on your own initiative. A win promotes the linked plan entry into the winners library; a rejection just records the answer. Confirm with the user before calling this, since a win writes to the library.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["verdict"],"properties":{"verdict":{"type":"string","enum":["winner","rejected"]},"title":{"type":"string"},"body":{"type":"string","description":"Optional replacement markdown for the library copy."},"servings":{"type":["integer","null"]},"tags":{"type":"array","items":{"type":"string"}},"cuisine":{"type":["string","null"]},"notes":{"type":["string","null"]},"rating":{"type":["integer","null"],"minimum":1,"maximum":5}}}}}},"responses":{"200":{"description":"The decided suggestion, plus the recipe a win created"},"409":{"description":"Not awaiting a verdict, or already decided"}}}},"/api/suggestions/{id}/retry":{"post":{"operationId":"retrySuggestion","summary":"Step a suggestion one stage back","description":"A candidate that never actually got cooked returns to the queue to be planned again. A verdict given by mistake returns to being asked -- and undoing a win archives the recipe that win created.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The suggestion, plus any recipe archived"},"409":{"description":"Already waiting to be planned; nothing to retry"}}}},"/api/entries/{id}/promote":{"post":{"operationId":"promoteEntryToWinner","summary":"Save a meal from a plan into the winners library","description":"Only when the household has said a meal was a hit. The web UI asks them about every cooked meal and a check there promotes it, so promoting unprompted fills the library with untested dinners. Copies the snapshot's markdown verbatim; pass a rewritten body for another serving size.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string","description":"Optional replacement markdown, e.g. a version rewritten back to a base serving size."},"servings":{"type":["integer","null"]},"tags":{"type":"array","items":{"type":"string"}},"cuisine":{"type":["string","null"]},"notes":{"type":["string","null"]},"rating":{"type":["integer","null"],"minimum":1,"maximum":5},"link_entry":{"type":"boolean","default":true}}}}}},"responses":{"201":{"description":"Created recipe plus the linked entry"}}}}}}