{"openapi":"3.0.0","info":{"title":"Al Dente","description":"An API for using Al Dente","version":"0.0.1"},"servers":[{"url":"https://api.aldente.cooking/","description":"API server"}],"tags":[{"name":"recipes","description":"Recipe related endpoints"},{"name":"collections","description":"Collection related endpoints"},{"name":"households","description":"Household related endpoints"},{"name":"meals","description":"Meal planning related endpoints"},{"name":"meal items","description":"What one meal holds — recipes and food named in words — changed one item at a time, with who each was for"},{"name":"people","description":"The people a household feeds who aren't members of it — family, friends, a helper like a nanny"},{"name":"preferences","description":"What the household, each member, and each of its people eat and won't, in their own words"},{"name":"pantry items","description":"The things a household keeps and restocks, fridge and freezer included, food or not"},{"name":"services","description":"The companies that get food to a household without it going anywhere"},{"name":"purchases","description":"The times a household bought something, with what each receipt said"},{"name":"pantry notes","description":"What happened to food on hand, in the household's words. The pantry itself is never stored; read a pantry item's history and judge"},{"name":"shopping list","description":"What the household means to buy, which people also call the grocery list, each linked to its pantry item and the meals it's for"},{"name":"tasks","description":"Work that goes into feeding the household, each linked to the meals it's for and given to a member or one of its people"},{"name":"equipment","description":"The kitchen equipment a household has, like a stand mixer or a rice cooker"},{"name":"places","description":"Somewhere food comes from or gets eaten, as the household calls it, like a market, a restaurant or a relative's kitchen"},{"name":"files","description":"File upload endpoints"},{"name":"feedback","description":"Feedback endpoint"}],"externalDocs":{"url":"https://developers.cookaldente.com","description":"Al Dente API Documentation"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API key","description":"An API key from Settings → API keys, sent as `Bearer <key>`. Read-only keys can make GET requests; read-write keys can make any request."}},"schemas":{},"parameters":{}},"paths":{"/rest/recipes":{"get":{"security":[{"bearerAuth":[]}],"tags":["recipes"],"summary":"List or search the authenticated user's recipes","description":"Returns recipe summaries for browsing. With `q`, returns only the recipes that match it, best match first. `collection_id`, `source_id` and `author` narrow the list, and combine with each other and with `q`. Send `limit` to page through it: the response's `X-Next-Cursor` header holds the `cursor` for the next page, and is absent on the last one. A search with `q` isn't paged. Use GET /recipes/:id to get full recipe details including ingredients and instructions.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"description":"Search the library in plain words, like \"lentil soup\" or \"something quick with chickpeas\". Results are ranked by how well they match; when the ranking is unavailable, recipes holding the query in their title, authors or source are returned instead, title matches first."},"required":false,"description":"Search the library in plain words, like \"lentil soup\" or \"something quick with chickpeas\". Results are ranked by how well they match; when the ranking is unavailable, recipes holding the query in their title, authors or source are returned instead, title matches first.","name":"q","in":"query"},{"schema":{"type":"string","description":"Only the recipes in this collection"},"required":false,"description":"Only the recipes in this collection","name":"collection_id","in":"query"},{"schema":{"type":"string","description":"Only the recipes from this source, such as a cookbook: the `id` of a recipe's `source`"},"required":false,"description":"Only the recipes from this source, such as a cookbook: the `id` of a recipe's `source`","name":"source_id","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Only the recipes by this author, matched by name, ignoring case"},"required":false,"description":"Only the recipes by this author, matched by name, ignoring case","name":"author","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"How many recipes to return, at most 100. Without it, every match is returned."},"required":false,"description":"How many recipes to return, at most 100. Without it, every match is returned.","name":"limit","in":"query"},{"schema":{"type":"string","description":"The `X-Next-Cursor` header from the previous page. Requires `limit`."},"required":false,"description":"The `X-Next-Cursor` header from the previous page. Requires `limit`.","name":"cursor","in":"query"}],"responses":{"200":{"description":"Successful response","headers":{"X-Next-Cursor":{"schema":{"type":"string","description":"With `limit`, the `cursor` for the next page; absent on the last page"},"required":false,"description":"With `limit`, the `cursor` for the next page; absent on the last page"}},"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"created_at":{"type":"string","format":"date-time"},"import_status":{"type":"string","enum":["importing","needs_review","ready"],"description":"Where the recipe's import stands. `importing` while it runs; `needs_review` when it settled without a complete recipe, with `import_reason` saying why; `ready` otherwise, including for a recipe that was never imported."},"import_reason":{"type":"string","nullable":true,"enum":["client_fetch_required","authentication_required","no_recipe_at_url","no_recipe_in_photos","site_unavailable","unknown_error","partial_photos","partial_photos_not_kept","missing_instructions","missing_ingredients","missing_amounts","unverified_speech","unverified_page","several_recipes","several_recipes_no_main",null],"description":"Why the import needs review. Null unless `import_status` is `needs_review`. More reasons may be added."},"latest_revision":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true},"source":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["url"]},"domain":{"type":"string"}},"required":["id","name","type","domain"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["cookbook"]},"isbn":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"subtitle":{"type":"string"}},"required":["id","name","type"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["other"]},"text":{"type":"string"}},"required":["id","name","type","text"],"description":"A source known only by its name — a restaurant, a person, a class, anything without a link or a book. `text` repeats `name`."},{"nullable":true}]},"citation":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"start_page_number":{"type":"integer"}},"required":["type"]},{"nullable":true}]},"image_url":{"type":"string","nullable":true}},"required":["id","title","source","citation","image_url"]}},"required":["id","created_at","import_status","import_reason","latest_revision"]}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["recipes"],"summary":"Create a new recipe","description":"Create a recipe. A `title` is required, and every other field is optional. A request with a `url` says with `import` whether to import the recipe from that page, which needs no title, or to save the recipe as sent with the url as where it came from. To import from photos of a recipe instead, upload them through POST /rest/files/import-photos and send their file IDs in `photo_file_ids`; that needs no title either. An import runs in the background, and the recipe reports its progress until it finishes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","nullable":true},"instructions":{"type":"array","nullable":true,"items":{"type":"object","properties":{"type":{"type":"string","enum":["item","heading"]},"text":{"type":"string"}},"required":["type","text"]}},"ingredients":{"type":"array","nullable":true,"items":{"type":"object","properties":{"type":{"type":"string","enum":["item","heading"]},"text":{"type":"string"}},"required":["type","text"]}},"url":{"type":"string","nullable":true,"description":"Write-only. The page the recipe comes from; read it back as `source`/`citation` in responses. Whether the recipe is imported from it is up to `import`."},"import":{"type":"boolean","description":"Whether to import the recipe from `url`. On create it is required whenever `url` is sent: true imports the page, false saves the recipe as sent with `url` as where it came from. On update, true re-imports the recipe from `url`. Sending `html` implies true."},"html":{"type":"string","nullable":true,"maxLength":5000000,"description":"Write-only. The recipe page's rendered HTML, for an import from `url` that reads this HTML instead of fetching the page. Requires `url`, and can't be sent with `import: false`."},"cook_time":{"type":"integer","nullable":true},"prep_time":{"type":"integer","nullable":true},"total_time":{"type":"integer","nullable":true},"authors":{"type":"array","nullable":true,"items":{"type":"string"}},"yield":{"type":"string","nullable":true,"description":"Free-text yield, parsed into a quantity and a unit."},"yield_quantity":{"type":"string","nullable":true},"yield_unit":{"type":"string","nullable":true},"source":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string","description":"The recipe's page"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"name":{"type":"string","minLength":1,"description":"The cookbook's title"},"isbn":{"type":"string","description":"ISBN-10 or ISBN-13"},"start_page_number":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"The page the recipe starts on"}},"required":["type","name"]},{"type":"object","properties":{"type":{"type":"string","enum":["other"]},"name":{"type":"string","minLength":1,"description":"What the recipe came from"}},"required":["type","name"]},{"nullable":true}],"description":"Where the recipe came from: a page (`url`), a cookbook, or `other`, something known only by its name, like a restaurant or a person. A `url` source records the link without importing it; to import, send `url` with `import: true` instead."},"notes":{"type":"string","nullable":true,"description":"Recipe notes, tips, or additional information. Plain text only; use \\n for line breaks."},"image_file_id":{"type":"string","minLength":1,"description":"A photo of the dish: the `recipe_photo` file ID POST /rest/files/recipe-photos/register returns."},"photo_file_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"minItems":1,"maxItems":5,"description":"Photos of the recipe to import instead of a url, like a cookbook page or a recipe card, in page order: file IDs from POST /rest/files/import-photos/register. At most 5. Implies `import: true`."}}}}}},"responses":{"201":{"description":"Recipe created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"collection_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the collections containing this recipe."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"latest_revision":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true},"instructions":{"type":"string","nullable":true},"ingredients":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"yield_quantity":{"type":"string","nullable":true},"yield_unit":{"type":"string","nullable":true},"prep_time":{"type":"integer","nullable":true},"cook_time":{"type":"integer","nullable":true},"authors":{"type":"array","nullable":true,"items":{"type":"string"}},"source":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["url"]},"domain":{"type":"string"}},"required":["id","name","type","domain"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["cookbook"]},"isbn":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"subtitle":{"type":"string"}},"required":["id","name","type"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["other"]},"text":{"type":"string"}},"required":["id","name","type","text"],"description":"A source known only by its name — a restaurant, a person, a class, anything without a link or a book. `text` repeats `name`."},{"nullable":true}]},"citation":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"start_page_number":{"type":"integer"}},"required":["type"]},{"nullable":true}]},"total_time":{"type":"integer","nullable":true},"creation_method":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Recipe notes, tips, or additional information. Plain text only; use \\n for line breaks."},"image_url":{"type":"string","nullable":true,"description":"URL of the recipe's main photo. Null when it has none."}},"required":["id","recipe_id"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"import_status":{"type":"string","enum":["importing","needs_review","ready"],"description":"Where the recipe's import stands. `importing` while it runs; `needs_review` when it settled without a complete recipe, with `import_reason` saying why; `ready` otherwise, including for a recipe that was never imported."},"import_reason":{"type":"string","nullable":true,"enum":["client_fetch_required","authentication_required","no_recipe_at_url","no_recipe_in_photos","site_unavailable","unknown_error","partial_photos","partial_photos_not_kept","missing_instructions","missing_ingredients","missing_amounts","unverified_speech","unverified_page","several_recipes","several_recipes_no_main",null],"description":"Why the import needs review. Null unless `import_status` is `needs_review`. More reasons may be added."},"cooking_history":{"type":"object","properties":{"times_cooked":{"type":"integer","description":"How many meals cooked at home included the recipe"},"leftovers_count":{"type":"integer","description":"How many meals of leftovers included it"},"last_cooked_on":{"type":"string","nullable":true,"description":"The last day it was cooked at home, as YYYY-MM-DD. Null when it never has been."},"meals":{"type":"array","items":{"type":"object","properties":{"meal_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"on":{"type":"string","description":"The day it was eaten, as YYYY-MM-DD"},"title":{"type":"string","nullable":true},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."}},"required":["meal_id","on","title","origin"]},"description":"Every cooked meal the recipe was part of, newest first. Plans aren't included."}},"required":["times_cooked","leftovers_count","last_cooked_on","meals"],"description":"How the household has eaten the recipe. Returned by GET /rest/recipes/{id}."}},"required":["id","collection_ids","author_id","latest_revision","created_at","updated_at","import_status","import_reason"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/recipes/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["recipes"],"summary":"Retrieve a specific recipe by ID","description":"Get a recipe by ID. Send `Accept: text/markdown` for the whole recipe as Markdown, the same text the MCP `get_recipe` tool returns; otherwise the response is JSON, and `format` is required.","parameters":[{"schema":{"type":"string","description":"Recipe ID"},"required":true,"description":"Recipe ID","name":"id","in":"path"},{"schema":{"type":"string","enum":["structured","markdown"],"description":"How ingredients and instructions are written inside the JSON body. Required unless the request accepts text/markdown."},"required":false,"description":"How ingredients and instructions are written inside the JSON body. Required unless the request accepts text/markdown.","name":"format","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"collection_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the collections containing this recipe."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"latest_revision":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true},"instructions":{"type":"string","nullable":true},"ingredients":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"yield_quantity":{"type":"string","nullable":true},"yield_unit":{"type":"string","nullable":true},"prep_time":{"type":"integer","nullable":true},"cook_time":{"type":"integer","nullable":true},"authors":{"type":"array","nullable":true,"items":{"type":"string"}},"source":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["url"]},"domain":{"type":"string"}},"required":["id","name","type","domain"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["cookbook"]},"isbn":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"subtitle":{"type":"string"}},"required":["id","name","type"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["other"]},"text":{"type":"string"}},"required":["id","name","type","text"],"description":"A source known only by its name — a restaurant, a person, a class, anything without a link or a book. `text` repeats `name`."},{"nullable":true}]},"citation":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"start_page_number":{"type":"integer"}},"required":["type"]},{"nullable":true}]},"total_time":{"type":"integer","nullable":true},"creation_method":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Recipe notes, tips, or additional information. Plain text only; use \\n for line breaks."},"image_url":{"type":"string","nullable":true,"description":"URL of the recipe's main photo. Null when it has none."}},"required":["id","recipe_id"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"import_status":{"type":"string","enum":["importing","needs_review","ready"],"description":"Where the recipe's import stands. `importing` while it runs; `needs_review` when it settled without a complete recipe, with `import_reason` saying why; `ready` otherwise, including for a recipe that was never imported."},"import_reason":{"type":"string","nullable":true,"enum":["client_fetch_required","authentication_required","no_recipe_at_url","no_recipe_in_photos","site_unavailable","unknown_error","partial_photos","partial_photos_not_kept","missing_instructions","missing_ingredients","missing_amounts","unverified_speech","unverified_page","several_recipes","several_recipes_no_main",null],"description":"Why the import needs review. Null unless `import_status` is `needs_review`. More reasons may be added."},"cooking_history":{"type":"object","properties":{"times_cooked":{"type":"integer","description":"How many meals cooked at home included the recipe"},"leftovers_count":{"type":"integer","description":"How many meals of leftovers included it"},"last_cooked_on":{"type":"string","nullable":true,"description":"The last day it was cooked at home, as YYYY-MM-DD. Null when it never has been."},"meals":{"type":"array","items":{"type":"object","properties":{"meal_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"on":{"type":"string","description":"The day it was eaten, as YYYY-MM-DD"},"title":{"type":"string","nullable":true},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."}},"required":["meal_id","on","title","origin"]},"description":"Every cooked meal the recipe was part of, newest first. Plans aren't included."}},"required":["times_cooked","leftovers_count","last_cooked_on","meals"],"description":"How the household has eaten the recipe. Returned by GET /rest/recipes/{id}."}},"required":["id","collection_ids","author_id","latest_revision","created_at","updated_at","import_status","import_reason"]}},"text/markdown":{"schema":{"type":"string"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["recipes"],"summary":"Delete a specific recipe by ID","description":"Delete a recipe by ID","parameters":[{"schema":{"type":"string","description":"Recipe ID"},"required":true,"description":"Recipe ID","name":"id","in":"path"}],"responses":{"204":{"description":"Recipe deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["recipes"],"summary":"Update a specific recipe by ID","description":"Update a recipe by ID with partial changes","parameters":[{"schema":{"type":"string","description":"Recipe ID"},"required":true,"description":"Recipe ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","nullable":true},"instructions":{"type":"array","nullable":true,"items":{"type":"object","properties":{"type":{"type":"string","enum":["item","heading"]},"text":{"type":"string"}},"required":["type","text"]}},"ingredients":{"type":"array","nullable":true,"items":{"type":"object","properties":{"type":{"type":"string","enum":["item","heading"]},"text":{"type":"string"}},"required":["type","text"]}},"url":{"type":"string","nullable":true,"description":"Write-only. The page the recipe comes from; read it back as `source`/`citation` in responses. With `import: true`, the recipe is re-imported from it. Send null to clear the link, keeping the recipe's source."},"import":{"type":"boolean","description":"Whether to import the recipe from `url`. On create it is required whenever `url` is sent: true imports the page, false saves the recipe as sent with `url` as where it came from. On update, true re-imports the recipe from `url`. Sending `html` implies true."},"html":{"type":"string","nullable":true,"maxLength":5000000,"description":"Write-only. The recipe page's rendered HTML, for an import from `url` that reads this HTML instead of fetching the page. Requires `url`, and can't be sent with `import: false`."},"cook_time":{"type":"integer","nullable":true,"description":"Send null to clear it."},"prep_time":{"type":"integer","nullable":true,"description":"Send null to clear it."},"total_time":{"type":"integer","nullable":true,"description":"Send null to clear it."},"authors":{"type":"array","nullable":true,"items":{"type":"string"},"description":"Send null to clear it."},"yield":{"type":"string","nullable":true,"description":"Free-text yield, parsed into a quantity and a unit. Send null to clear it. That also clears `yield_quantity` and `yield_unit`, unless you send those too."},"yield_quantity":{"type":"string","nullable":true,"description":"Send null to clear it."},"yield_unit":{"type":"string","nullable":true,"description":"Send null to clear it."},"source":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string","description":"The recipe's page"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"name":{"type":"string","minLength":1,"description":"The cookbook's title"},"isbn":{"type":"string","description":"ISBN-10 or ISBN-13"},"start_page_number":{"type":"integer","minimum":0,"exclusiveMinimum":true,"description":"The page the recipe starts on"}},"required":["type","name"]},{"type":"object","properties":{"type":{"type":"string","enum":["other"]},"name":{"type":"string","minLength":1,"description":"What the recipe came from"}},"required":["type","name"]},{"nullable":true}],"description":"Where the recipe came from: a page, a cookbook, or a name only. Send null to remove the recipe's source."},"notes":{"type":"string","nullable":true,"description":"Recipe notes, tips, or additional information. Plain text only; use \\n for line breaks. Send null to clear it."},"image_file_id":{"type":"string","nullable":true,"minLength":1,"description":"Replace the recipe's photo with this `recipe_photo` file ID from POST /rest/files/recipe-photos/register, or send null to remove it."},"creation_method":{"type":"string","enum":["manual","import"]}}}}}},"responses":{"200":{"description":"Recipe updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"collection_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the collections containing this recipe."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"latest_revision":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true},"instructions":{"type":"string","nullable":true},"ingredients":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"yield_quantity":{"type":"string","nullable":true},"yield_unit":{"type":"string","nullable":true},"prep_time":{"type":"integer","nullable":true},"cook_time":{"type":"integer","nullable":true},"authors":{"type":"array","nullable":true,"items":{"type":"string"}},"source":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["url"]},"domain":{"type":"string"}},"required":["id","name","type","domain"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["cookbook"]},"isbn":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"subtitle":{"type":"string"}},"required":["id","name","type"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"type":{"type":"string","enum":["other"]},"text":{"type":"string"}},"required":["id","name","type","text"],"description":"A source known only by its name — a restaurant, a person, a class, anything without a link or a book. `text` repeats `name`."},{"nullable":true}]},"citation":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["url"]},"url":{"type":"string"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","enum":["cookbook"]},"start_page_number":{"type":"integer"}},"required":["type"]},{"nullable":true}]},"total_time":{"type":"integer","nullable":true},"creation_method":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true,"description":"Recipe notes, tips, or additional information. Plain text only; use \\n for line breaks."},"image_url":{"type":"string","nullable":true,"description":"URL of the recipe's main photo. Null when it has none."}},"required":["id","recipe_id"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"import_status":{"type":"string","enum":["importing","needs_review","ready"],"description":"Where the recipe's import stands. `importing` while it runs; `needs_review` when it settled without a complete recipe, with `import_reason` saying why; `ready` otherwise, including for a recipe that was never imported."},"import_reason":{"type":"string","nullable":true,"enum":["client_fetch_required","authentication_required","no_recipe_at_url","no_recipe_in_photos","site_unavailable","unknown_error","partial_photos","partial_photos_not_kept","missing_instructions","missing_ingredients","missing_amounts","unverified_speech","unverified_page","several_recipes","several_recipes_no_main",null],"description":"Why the import needs review. Null unless `import_status` is `needs_review`. More reasons may be added."},"cooking_history":{"type":"object","properties":{"times_cooked":{"type":"integer","description":"How many meals cooked at home included the recipe"},"leftovers_count":{"type":"integer","description":"How many meals of leftovers included it"},"last_cooked_on":{"type":"string","nullable":true,"description":"The last day it was cooked at home, as YYYY-MM-DD. Null when it never has been."},"meals":{"type":"array","items":{"type":"object","properties":{"meal_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"on":{"type":"string","description":"The day it was eaten, as YYYY-MM-DD"},"title":{"type":"string","nullable":true},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."}},"required":["meal_id","on","title","origin"]},"description":"Every cooked meal the recipe was part of, newest first. Plans aren't included."}},"required":["times_cooked","leftovers_count","last_cooked_on","meals"],"description":"How the household has eaten the recipe. Returned by GET /rest/recipes/{id}."}},"required":["id","collection_ids","author_id","latest_revision","created_at","updated_at","import_status","import_reason"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/collections":{"get":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"List all collections for the authenticated user","description":"Returns every collection in the authenticated user's households, each with the IDs of the recipes it contains. Use GET /rest/recipes/{id} to get full recipe details.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Create a collection","description":"Creates an empty collection in the authenticated user's household. Add recipes with POST /rest/collections/{id}/recipes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50,"description":"Collection name (max 50 characters)"}},"required":["name"]}}}},"responses":{"201":{"description":"Collection created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/cookbooks":{"get":{"security":[{"bearerAuth":[]}],"tags":["cookbooks"],"summary":"List the household's cookbooks","description":"Returns every cookbook the household's recipes come from, alphabetically. List a cookbook's recipes with GET /rest/recipes?source_id={id}.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"isbn":{"type":"string","nullable":true},"cover_url":{"type":"string","nullable":true},"recipe_count":{"type":"integer","description":"How many of the household's recipes come from it"}},"required":["id","name","isbn","cover_url","recipe_count"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/cookbooks/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["cookbooks"],"summary":"Get one of the household's cookbooks","description":"Returns one cookbook by its ID, the `id` of a cookbook source on a recipe. List its recipes with GET /rest/recipes?source_id={id}.","parameters":[{"schema":{"type":"string","description":"Cookbook ID"},"required":true,"description":"Cookbook ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"subtitle":{"type":"string","nullable":true},"isbn":{"type":"string","nullable":true},"authors":{"type":"array","items":{"type":"string"},"description":"The people who wrote it. Empty when it is credited to a brand."},"brand":{"type":"string","nullable":true,"description":"A test kitchen, magazine or publisher the book is credited to instead of people"},"cover_url":{"type":"string","nullable":true}},"required":["id","name","subtitle","isbn","authors","brand","cover_url"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/authors":{"get":{"security":[{"bearerAuth":[]}],"tags":["authors"],"summary":"List the authors of the household's recipes","description":"Returns everyone credited as an author on the household's recipes, alphabetically, with how many recipes each wrote. List an author's recipes with GET /rest/recipes?author={name}.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"recipe_count":{"type":"integer","description":"How many of the household's recipes credit them"}},"required":["name","recipe_count"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/collections/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Get one collection","description":"Returns one collection in the authenticated user's households, with the IDs of the recipes it contains. Use GET /rest/recipes/{id} to get full recipe details.","parameters":[{"schema":{"type":"string","description":"Collection ID"},"required":true,"description":"Collection ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Rename a collection","description":"Renames a collection in the authenticated user's households. Recipe membership is left unchanged.","parameters":[{"schema":{"type":"string","description":"Collection ID"},"required":true,"description":"Collection ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50,"description":"New collection name (max 50 characters)"}},"required":["name"]}}}},"responses":{"200":{"description":"Collection renamed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Delete a collection","description":"Deletes a collection in the authenticated user's households. Recipes in the collection are kept. Missing collections and collections in another household return 404.","parameters":[{"schema":{"type":"string","description":"Collection ID"},"required":true,"description":"Collection ID","name":"id","in":"path"}],"responses":{"204":{"description":"Collection deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/collections/{id}/recipes":{"post":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Add a recipe to a collection","description":"Adds a recipe to a collection and returns the updated collection. Both must belong to the same household, accessible to the authenticated user. Idempotent: adding a recipe already in the collection succeeds without duplicating it. Missing or inaccessible collections and recipes return the same 404.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Collection ID"},"required":true,"description":"Collection ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recipe_id":{"type":"string","minLength":1,"description":"ID of a recipe in the collection's household, from GET /rest/recipes."}},"required":["recipe_id"]}}}},"responses":{"200":{"description":"Updated collection, including recipe_ids","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/collections/{id}/recipes/{recipe_id}":{"delete":{"security":[{"bearerAuth":[]}],"tags":["collections"],"summary":"Remove a recipe from a collection","description":"Removes a recipe from a collection and returns the updated collection. The recipe itself is kept. Both must belong to the same household, accessible to the authenticated user. Idempotent: removing a recipe not in the collection also succeeds. Missing or inaccessible collections and recipes return the same 404.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Collection ID"},"required":true,"description":"Collection ID","name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"description":"Recipe ID"},"required":true,"description":"Recipe ID","name":"recipe_id","in":"path"}],"responses":{"200":{"description":"Updated collection, including recipe_ids","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"recipe_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"description":"IDs of the recipes in this collection, in the order they were added. Add with POST /rest/collections/{id}/recipes; remove with DELETE /rest/collections/{id}/recipes/{recipe_id}. Fetch a recipe's details from GET /rest/recipes/{id}."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","recipe_ids","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals":{"get":{"security":[{"bearerAuth":[]}],"tags":["meals"],"summary":"List meals across a range of days","description":"Returns cooked meal logs and uncooked plans in the authenticated user's household between two dates, inclusive of both ends, in `when` order. The cooked boolean distinguishes what happened from what is only intended. An unplanned meal (`when` of null) has no day, so it is never in a range. A day with no meals simply has none in the response.\n\nBoth dates are required and the range may span at most 92 days. There is no default: dates are calendar days with no time zone, and the server does not decide what \"today\" is.","parameters":[{"schema":{"type":"string","description":"First day to include, as YYYY-MM-DD. Inclusive."},"required":true,"description":"First day to include, as YYYY-MM-DD. Inclusive.","name":"start","in":"query"},{"schema":{"type":"string","description":"Last day to include, as YYYY-MM-DD. Inclusive."},"required":true,"description":"Last day to include, as YYYY-MM-DD. Inclusive.","name":"end","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"cooked":{"type":"boolean","description":"Whether the meal has happened. True is a logged meal; false is an uncooked plan."},"when":{"type":"string","nullable":true,"description":"When the meal is, as an ISO 8601 calendar date, YYYY-MM-DD. A cooked meal is always on a date. An uncooked plan is on a date, or null when it's unplanned: nobody has picked a day yet. Dates have no time zone."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"description":"Which part of the day the meal was, or null when it wasn't filed under one."},"title":{"type":"string","nullable":true,"description":"What someone called this meal — \"Parents coming over\". Null when the recipe items say it all."},"description":{"type":"string","nullable":true,"description":"A description of the meal, or null when nobody wrote one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens or happened. Null when nobody said."},"photos":{"type":"array","items":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"url":{"type":"string","format":"uri"}},"required":["file_id","url"]},"description":"The meal's photos, in the order they read — the first is the one a card draws. A plan can carry photos too, such as a picture of what to make."},"items":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]},"description":"What the meal holds, in the order it reads. Fetch a recipe item's details from GET /rest/recipes/{id}."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Who ate the meal, or plans to: household members as `{user_id}` and the household's people as `{person_id}`. Empty when nobody was recorded."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","cooked","when","origin","daypart","title","description","place_id","photos","items","participants","created_at","updated_at"]}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["meals"],"summary":"Create a meal","description":"Creates one meal in the authenticated user's household. `cooked: false` — the default — plans a meal: set `when` to a calendar date (YYYY-MM-DD), or set it to null or omit it to leave the plan unplanned, with no day picked yet. `cooked: true` records a meal that already happened, which needs a date in `when` and a `title`, a photo, or a recipe item to say what was eaten. Either kind can carry uploaded `photo_file_ids`, a title, an ordered list of recipes, and who ate the meal or plans to. To mark an existing plan cooked, use PUT /rest/meals/{id}.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cooked":{"type":"boolean","default":false,"description":"Whether the meal has already happened. False — the default — creates an uncooked plan. True records a meal that happened, which needs a date in `when` and something saying what was eaten: a `title`, a photo, or a recipe item."},"when":{"type":"string","nullable":true,"description":"When the meal is. Required when `cooked` is true, and then a date (YYYY-MM-DD). A plan takes a date, or null or nothing to leave it unplanned."},"kind":{"description":"Removed. Renamed to `origin`; a request that sends it is rejected."},"participant_ids":{"description":"Removed. Replaced by `participants`; a request that sends it is rejected."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"default":null,"description":"Which part of the day the meal is. Null to leave it unfiled."},"title":{"type":"string","maxLength":500,"description":"What to call the meal. Optional: leave it out when the items or photos say everything. A cooked meal with no title needs a photo or a recipe item."},"description":{"type":"string","maxLength":5000,"description":"A description of the meal — what it is or was, how it's served, who it's for, or for a plan the options still being weighed. 5000 characters or less."},"place_id":{"type":"string","minLength":1,"description":"Where it happens, from GET /rest/places."},"photo_file_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":10,"default":[],"description":"The meal_photo file IDs returned by POST /rest/files/meal-photos/register, in the order they should read. This is the whole list, not an addition to it. At most 10, with no repeats."},"items":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A recipe ID from GET /rest/recipes."}},"required":["recipe_id"]},{"type":"object","properties":{"type":{"type":"string","enum":["named"]},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Food named in words rather than linked to a recipe — \"Takeout pad thai\", \"A baguette from the market\". 200 characters or less."}},"required":["type","name"]}]},"default":[],"description":"What the meal holds, in the order it should read: recipes as `{\"type\": \"recipe\", \"recipe_id\": \"...\"}` and food without a recipe as `{\"type\": \"named\", \"name\": \"Takeout pad thai\"}`. This is the whole list, not an addition to it: pass every item the meal should end up with, or an empty array to empty it. To change one item without resending the rest, use the /rest/meals/{id}/items routes."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"default":[],"description":"Who ate the meal, or plans to: household members as `{user_id}` and the household's people as `{person_id}`. A plan can name them too — a household usually knows who a meal is for before it happens."}},"required":["origin"]}}}},"responses":{"201":{"description":"Meal created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"cooked":{"type":"boolean","description":"Whether the meal has happened. True is a logged meal; false is an uncooked plan."},"when":{"type":"string","nullable":true,"description":"When the meal is, as an ISO 8601 calendar date, YYYY-MM-DD. A cooked meal is always on a date. An uncooked plan is on a date, or null when it's unplanned: nobody has picked a day yet. Dates have no time zone."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"description":"Which part of the day the meal was, or null when it wasn't filed under one."},"title":{"type":"string","nullable":true,"description":"What someone called this meal — \"Parents coming over\". Null when the recipe items say it all."},"description":{"type":"string","nullable":true,"description":"A description of the meal, or null when nobody wrote one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens or happened. Null when nobody said."},"photos":{"type":"array","items":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"url":{"type":"string","format":"uri"}},"required":["file_id","url"]},"description":"The meal's photos, in the order they read — the first is the one a card draws. A plan can carry photos too, such as a picture of what to make."},"items":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]},"description":"What the meal holds, in the order it reads. Fetch a recipe item's details from GET /rest/recipes/{id}."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Who ate the meal, or plans to: household members as `{user_id}` and the household's people as `{person_id}`. Empty when nobody was recorded."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","cooked","when","origin","daypart","title","description","place_id","photos","items","participants","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals/unplanned":{"get":{"security":[{"bearerAuth":[]}],"tags":["meals"],"summary":"List unplanned meals","description":"Returns the uncooked plans in the authenticated user's household that have no day yet (`when` of null), in the order they were made. GET /rest/meals reads a range of days, so it never returns these. To give one a day, set `when` with PUT /rest/meals/{id}.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"cooked":{"type":"boolean","description":"Whether the meal has happened. True is a logged meal; false is an uncooked plan."},"when":{"type":"string","nullable":true,"description":"When the meal is, as an ISO 8601 calendar date, YYYY-MM-DD. A cooked meal is always on a date. An uncooked plan is on a date, or null when it's unplanned: nobody has picked a day yet. Dates have no time zone."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"description":"Which part of the day the meal was, or null when it wasn't filed under one."},"title":{"type":"string","nullable":true,"description":"What someone called this meal — \"Parents coming over\". Null when the recipe items say it all."},"description":{"type":"string","nullable":true,"description":"A description of the meal, or null when nobody wrote one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens or happened. Null when nobody said."},"photos":{"type":"array","items":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"url":{"type":"string","format":"uri"}},"required":["file_id","url"]},"description":"The meal's photos, in the order they read — the first is the one a card draws. A plan can carry photos too, such as a picture of what to make."},"items":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]},"description":"What the meal holds, in the order it reads. Fetch a recipe item's details from GET /rest/recipes/{id}."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Who ate the meal, or plans to: household members as `{user_id}` and the household's people as `{person_id}`. Empty when nobody was recorded."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","cooked","when","origin","daypart","title","description","place_id","photos","items","participants","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals/{id}":{"put":{"security":[{"bearerAuth":[]}],"tags":["meals"],"summary":"Update a meal by ID","description":"Changes a cooked meal log or uncooked plan, including moving it to another day, making a plan unplanned by setting `when` to null, or recording that it happened. A cooked log must stay on a date and cannot be changed back into an uncooked plan.\n\nEvery field is optional and an omitted one is left alone. `items` and `photo_file_ids` each replace their whole list rather than adding to it, so one call produces one resulting state and a meal's parts can't drift out of step with each other. An empty `photo_file_ids` list removes every photo.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cooked":{"type":"boolean","description":"Set true when an uncooked plan has happened. False may be repeated for an existing plan, but a cooked log cannot be changed back into a plan. Marking a meal cooked requires it to be on a date and to have a title, a photo, or a recipe item."},"when":{"type":"string","nullable":true,"description":"Move the meal to this date (YYYY-MM-DD), or set null to make an uncooked plan unplanned. A cooked meal must be on a date. Omit the field to leave it alone."},"kind":{"description":"Removed. Renamed to `origin`; a request that sends it is rejected."},"participant_ids":{"description":"Removed. Replaced by `participants`; a request that sends it is rejected."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"description":"Which part of the day the meal was. Null unfiles it; omit the field to leave it as it is."},"title":{"type":"string","nullable":true,"maxLength":500,"description":"Rename the meal, or null to clear its title. A cooked meal with no title must keep a photo or a recipe item."},"description":{"type":"string","nullable":true,"maxLength":5000,"description":"Replace the meal's description, or null to clear it. Omit the field to leave it alone."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens, from GET /rest/places, or null to clear it. Omit the field to leave it alone."},"items":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A recipe ID from GET /rest/recipes."}},"required":["recipe_id"]},{"type":"object","properties":{"type":{"type":"string","enum":["named"]},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Food named in words rather than linked to a recipe — \"Takeout pad thai\", \"A baguette from the market\". 200 characters or less."}},"required":["type","name"]}]},"description":"What the meal holds, in the order it should read: recipes as `{\"type\": \"recipe\", \"recipe_id\": \"...\"}` and food without a recipe as `{\"type\": \"named\", \"name\": \"Takeout pad thai\"}`. This is the whole list, not an addition to it: pass every item the meal should end up with, or an empty array to empty it. To change one item without resending the rest, use the /rest/meals/{id}/items routes."},"photo_file_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":10,"description":"Replace the meal's photos with these meal_photo file IDs, in the order they should read. Omit the field to leave the photos alone, or pass an empty list to remove every photo. At most 10, with no repeats."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Who ate the meal, or plans to, replacing the whole set. Settable on an uncooked plan as well as a cooked log."}}}}}},"responses":{"200":{"description":"Meal updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"cooked":{"type":"boolean","description":"Whether the meal has happened. True is a logged meal; false is an uncooked plan."},"when":{"type":"string","nullable":true,"description":"When the meal is, as an ISO 8601 calendar date, YYYY-MM-DD. A cooked meal is always on a date. An uncooked plan is on a date, or null when it's unplanned: nobody has picked a day yet. Dates have no time zone."},"origin":{"type":"string","enum":["cooking","takeout","leftovers","out"],"description":"Where the meal comes from: cooked at home, takeout, leftovers, or eaten out. A meal is exactly one of these; a night that is both takeout and cooking is two meals on the same day."},"daypart":{"type":"string","nullable":true,"enum":["breakfast","lunch","dinner",null],"description":"Which part of the day the meal was, or null when it wasn't filed under one."},"title":{"type":"string","nullable":true,"description":"What someone called this meal — \"Parents coming over\". Null when the recipe items say it all."},"description":{"type":"string","nullable":true,"description":"A description of the meal, or null when nobody wrote one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens or happened. Null when nobody said."},"photos":{"type":"array","items":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"url":{"type":"string","format":"uri"}},"required":["file_id","url"]},"description":"The meal's photos, in the order they read — the first is the one a card draws. A plan can carry photos too, such as a picture of what to make."},"items":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]},"description":"What the meal holds, in the order it reads. Fetch a recipe item's details from GET /rest/recipes/{id}."},"participants":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Who ate the meal, or plans to: household members as `{user_id}` and the household's people as `{person_id}`. Empty when nobody was recorded."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","cooked","when","origin","daypart","title","description","place_id","photos","items","participants","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["meals"],"summary":"Delete a meal by ID","description":"Deletes a cooked meal log or uncooked plan. The recipes it was attached to are untouched. Any attached photos and participant records are deleted with it. Idempotent: a meal that is already gone answers 204 as well, so a retry is safe.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"}],"responses":{"204":{"description":"Meal deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals/{id}/items":{"get":{"security":[{"bearerAuth":[]}],"tags":["meal items"],"summary":"List a meal's items","description":"Everything in one meal, cooked or planned, in the order it reads — recipes and food named in words — each with its ID and who it was for when that wasn't everyone at the meal.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"}],"responses":{"200":{"description":"The meal's items","content":{"application/json":{"schema":{"type":"array","items":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["meal items"],"summary":"Add an item to a meal","description":"Adds one thing to a meal, leaving the rest alone: a recipe as `recipe_id`, or food named in words as `name` — \"Takeout pad thai\" — when there is no recipe. Exactly one of the two. It goes at `position`, or at the end.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recipe_id":{"type":"string","minLength":1,"description":"A recipe ID from GET /rest/recipes."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Food named in words rather than linked to a recipe — \"Takeout pad thai\", \"A baguette from the market\". 200 characters or less."},"position":{"type":"integer","minimum":0,"description":"Where the item goes among the meal's items, counted from zero. Past the end, or omitted on create, means the end."}}}}}},"responses":{"201":{"description":"Item added","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals/{id}/items/{item_id}":{"put":{"security":[{"bearerAuth":[]}],"tags":["meal items"],"summary":"Update a meal item","description":"Changes one item of a meal: `recipe_id` makes it that recipe, `name` makes it food named in words — so a named item can be renamed, or turned into a recipe and back — and `position` moves it. Send at least one, and never both `recipe_id` and `name`. The item keeps who it was for.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"},{"schema":{"type":"string","description":"Meal item ID"},"required":true,"description":"Meal item ID","name":"item_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recipe_id":{"type":"string","minLength":1,"description":"A recipe ID from GET /rest/recipes."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Food named in words rather than linked to a recipe — \"Takeout pad thai\", \"A baguette from the market\". 200 characters or less."},"position":{"type":"integer","minimum":0,"description":"Where the item goes among the meal's items, counted from zero. Past the end, or omitted on create, means the end."}}}}}},"responses":{"200":{"description":"Item updated","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["meal items"],"summary":"Remove an item from a meal","description":"Takes one item out of a meal, along with who it was for. The recipe itself stays in the library. An item already gone answers 204 too. A cooked meal must still say what was eaten afterwards — a title, a photo, or another item — or this answers 400.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"},{"schema":{"type":"string","description":"Meal item ID"},"required":true,"description":"Meal item ID","name":"item_id","in":"path"}],"responses":{"204":{"description":"Item removed"},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/meals/{id}/items/{item_id}/eaters":{"put":{"security":[{"bearerAuth":[]}],"tags":["meal items"],"summary":"Set who a meal item was for","description":"Records who one item of a meal was for when it wasn't everyone at the meal — the nuggets were for the kid — replacing the whole set. An empty `eaters` list means everyone again. Only current members and the household's people who aren't archived can be added.","parameters":[{"schema":{"type":"string","description":"Meal ID"},"required":true,"description":"Meal ID","name":"id","in":"path"},{"schema":{"type":"string","description":"Meal item ID"},"required":true,"description":"Meal item ID","name":"item_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}]},"description":"Everyone this item was for, replacing the whole set: members as `{user_id}` and the household's people as `{person_id}`. An empty array means everyone at the meal."}},"required":["eaters"]}}}},"responses":{"200":{"description":"Eaters set","content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["recipe"]},"recipe_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"servings":{"type":"string","nullable":true},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","recipe_id","servings","eaters"]},{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"The item's ID, for the /rest/meals/{id}/items routes."},"type":{"type":"string","enum":["named"]},"name":{"type":"string"},"eaters":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","nullable":true,"minLength":1,"description":"A household member's user ID, or null for a member who has since deleted their account."}},"required":["user_id"]},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of one of the household's people, from GET /rest/people."}},"required":["person_id"]}]},"description":"Who this item was for when it wasn't everyone at the meal — the nuggets were for the kid. Empty means everyone."}},"required":["id","type","name","eaters"]}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/meal-photos":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Create a meal photo upload","description":"Starts a meal photo upload for the authenticated user's household. PUT the photo bytes directly to `upload_url` using the same Content-Type, then pass `upload_key` to POST /rest/files/meal-photos/register. The upload URL is temporary and can be used once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/jpeg","image/jpg","image/png","image/heic","image/heif","image/webp"],"description":"The Content-Type the photo will use when uploaded."}},"required":["content_type"]}}}},"responses":{"201":{"description":"Meal photo upload created","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"type":"string","format":"uri"},"upload_key":{"type":"string","description":"Opaque key to send to the register endpoint after the upload succeeds."}},"required":["upload_url","upload_key"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/meal-photos/register":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Register an uploaded meal photo","description":"Finishes a meal photo upload after its PUT succeeds. Returns the `meal_photo` file ID to use in `photo_file_ids` on POST /rest/meals or PUT /rest/meals/{id}.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"upload_key":{"type":"string","minLength":1,"description":"The opaque upload key returned when the upload was created."}},"required":["upload_key"]}}}},"responses":{"201":{"description":"Meal photo registered","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."}},"required":["file_id"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/recipe-photos":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Create a recipe photo upload","description":"Starts a recipe photo upload for the authenticated user's household. PUT the photo bytes directly to `upload_url` using the same Content-Type, then pass `upload_key` to POST /rest/files/recipe-photos/register. The upload URL is temporary and can be used once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/jpeg","image/jpg","image/png","image/heic","image/heif","image/webp"],"description":"The Content-Type the photo will use when uploaded."}},"required":["content_type"]}}}},"responses":{"201":{"description":"Recipe photo upload created","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"type":"string","format":"uri"},"upload_key":{"type":"string","description":"Opaque key to send to the register endpoint after the upload succeeds."}},"required":["upload_url","upload_key"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/recipe-photos/register":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Register an uploaded recipe photo","description":"Finishes a recipe photo upload after its PUT succeeds. Returns the `recipe_photo` file ID to use as `image_file_id` on POST /rest/recipes or PUT /rest/recipes/{id}.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"upload_key":{"type":"string","minLength":1,"description":"The opaque upload key returned when the upload was created."}},"required":["upload_key"]}}}},"responses":{"201":{"description":"Recipe photo registered","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."}},"required":["file_id"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/person-photos":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Create a person photo upload","description":"Starts a person photo upload for the authenticated user's household. PUT the photo bytes directly to `upload_url` using the same Content-Type, then pass `upload_key` to POST /rest/files/person-photos/register. The upload URL is temporary and can be used once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/jpeg","image/jpg","image/png","image/heic","image/heif","image/webp"],"description":"The Content-Type the photo will use when uploaded."}},"required":["content_type"]}}}},"responses":{"201":{"description":"Person photo upload created","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"type":"string","format":"uri"},"upload_key":{"type":"string","description":"Opaque key to send to the register endpoint after the upload succeeds."}},"required":["upload_url","upload_key"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/person-photos/register":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Register an uploaded person photo","description":"Finishes a person photo upload after its PUT succeeds. Returns the `person_photo` file ID to use as `photo_file_id` on POST /rest/people or PUT /rest/people/{id}.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"upload_key":{"type":"string","minLength":1,"description":"The opaque upload key returned when the upload was created."}},"required":["upload_key"]}}}},"responses":{"201":{"description":"Person photo registered","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."}},"required":["file_id"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/import-photos":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Create an upload for a photo of a recipe to import","description":"Starts uploading a photo of a recipe to import, like a cookbook page or a recipe card, for the authenticated user's household. PUT the photo bytes directly to `upload_url` using the same Content-Type, then pass `upload_key` to POST /rest/files/import-photos/register. The upload URL is temporary and can be used once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content_type":{"type":"string","enum":["image/jpeg","image/jpg","image/png","image/heic","image/heif","image/webp"],"description":"The Content-Type the photo will use when uploaded."}},"required":["content_type"]}}}},"responses":{"201":{"description":"Import photo upload created","content":{"application/json":{"schema":{"type":"object","properties":{"upload_url":{"type":"string","format":"uri"},"upload_key":{"type":"string","description":"Opaque key to send to the register endpoint after the upload succeeds."}},"required":["upload_url","upload_key"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/files/import-photos/register":{"post":{"security":[{"bearerAuth":[]}],"tags":["files"],"summary":"Register an uploaded photo of a recipe to import","description":"Finishes uploading a photo of a recipe to import after its PUT succeeds. The photo is read by the import and then discarded. Returns the file ID to use in `photo_file_ids` on POST /rest/recipes with `import: true`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"upload_key":{"type":"string","minLength":1,"description":"The opaque upload key returned when the upload was created."}},"required":["upload_key"]}}}},"responses":{"201":{"description":"Import photo registered","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string","minLength":1,"description":"A Convex document ID."}},"required":["file_id"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/people":{"get":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"List the household's people","description":"Returns the people the authenticated user's household feeds who aren't members of it — family, friends, a helper like a nanny — in the order they were added, each with what they are to each member who has said and their food preferences. Household members themselves aren't included. Archived people are left out unless `include_archived` is `true`.","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"Include archived people. Defaults to false."},"required":false,"description":"Include archived people. Defaults to false.","name":"include_archived","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"What they eat and won't. Null when nobody has said."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each household member who has said. A member with no entry hasn't said."},"photo_url":{"type":"string","nullable":true,"description":"A photo of them, or null when there is none."},"archived":{"type":"boolean","description":"Archived people are left out of lists and can't be added to meals, but stay on meals that already name them."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","preferences","relations","photo_url","archived","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Add a person the household feeds","description":"Adds someone the authenticated user's household feeds who isn't a member of it — family, friends, a helper like a nanny. `relations` should say what they are to every current member, not only the caller, since a member with no relation recorded can't refer to them by one; a member named twice keeps the last relation given. Household members are not added this way — they join by invitation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls them, like Margaret or Grandma."},"preferences":{"type":"string","maxLength":2000,"description":"What they eat, what they won't, and what they can't, in free text. Blank is stored as no preferences."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each current household member. Send one for every member: a member with no entry can't refer to this person by relation. Record only what someone has said; one member's relation says nothing about another's, so ask rather than derive one."},"photo_file_id":{"type":"string","minLength":1,"description":"A photo of them: the `person_photo` file ID POST /rest/files/person-photos/register returns."}},"required":["name"]}}}},"responses":{"201":{"description":"Person created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"What they eat and won't. Null when nobody has said."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each household member who has said. A member with no entry hasn't said."},"photo_url":{"type":"string","nullable":true,"description":"A photo of them, or null when there is none."},"archived":{"type":"boolean","description":"Archived people are left out of lists and can't be added to meals, but stay on meals that already name them."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","preferences","relations","photo_url","archived","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/people/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Get one of the household's people","description":"Returns one person the household feeds who isn't a member, archived or not, with their relations to each member and their food preferences.","parameters":[{"schema":{"type":"string","description":"Person ID"},"required":true,"description":"Person ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"What they eat and won't. Null when nobody has said."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each household member who has said. A member with no entry hasn't said."},"photo_url":{"type":"string","nullable":true,"description":"A photo of them, or null when there is none."},"archived":{"type":"boolean","description":"Archived people are left out of lists and can't be added to meals, but stay on meals that already name them."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","preferences","relations","photo_url","archived","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Update one of the household's people","description":"Changes someone's name, preferences or photo, or archives or unarchives them. Every field is optional and an omitted one is left alone. Archiving is how to take someone who is on a meal off the list, since they can't be deleted. Relations have their own routes under /rest/people/{id}/relations, so one member's answer never overwrites another's.","parameters":[{"schema":{"type":"string","description":"Person ID"},"required":true,"description":"Person ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls them, like Margaret or Grandma."},"preferences":{"type":"string","nullable":true,"maxLength":2000,"description":"What they eat and won't. Send null to clear it."},"archived":{"type":"boolean","description":"True archives them, false brings them back."},"photo_file_id":{"type":"string","nullable":true,"minLength":1,"description":"Replace their photo with this `person_photo` file ID from POST /rest/files/person-photos/register, or send null to remove it."}}}}}},"responses":{"200":{"description":"Person updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"What they eat and won't. Null when nobody has said."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each household member who has said. A member with no entry hasn't said."},"photo_url":{"type":"string","nullable":true,"description":"A photo of them, or null when there is none."},"archived":{"type":"boolean","description":"Archived people are left out of lists and can't be added to meals, but stay on meals that already name them."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","preferences","relations","photo_url","archived","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Delete one of the household's people","description":"Deletes someone the household added, along with their relations. Refused with 409 while any meal, planned or cooked, names them — archive them instead with PUT /rest/people/{id} and `{\"archived\": true}`, which takes them off the list without changing who was at past meals.","parameters":[{"schema":{"type":"string","description":"Person ID"},"required":true,"description":"Person ID","name":"id","in":"path"}],"responses":{"204":{"description":"Person deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/people/{id}/relations/{user_id}":{"put":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Set what someone is to one household member","description":"Records what this person is to one member of the household — the same person is often one member's parent and another's parent-in-law — replacing whatever that member had before and leaving every other member's relation alone.","parameters":[{"schema":{"type":"string","description":"Person ID"},"required":true,"description":"Person ID","name":"id","in":"path"},{"schema":{"type":"string","description":"The household member's user ID"},"required":true,"description":"The household member's user ID","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["relation"]}}}},"responses":{"200":{"description":"Relation set successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"What they eat and won't. Null when nobody has said."},"relations":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID."},"relation":{"type":"string","minLength":1,"maxLength":100,"description":"What this person is to the household member, read as \"{person} is {member}'s {relation}\". Freeform text in the household's words, like mom, family friend, or Carolina's husband."}},"required":["user_id","relation"]},"description":"What this person is to each household member who has said. A member with no entry hasn't said."},"photo_url":{"type":"string","nullable":true,"description":"A photo of them, or null when there is none."},"archived":{"type":"boolean","description":"Archived people are left out of lists and can't be added to meals, but stay on meals that already name them."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","preferences","relations","photo_url","archived","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["people"],"summary":"Clear what someone is to one household member","description":"Forgets what this person is to one member, leaving every other member's relation alone. Idempotent: clearing a relation nobody set answers 204 as well.","parameters":[{"schema":{"type":"string","description":"Person ID"},"required":true,"description":"Person ID","name":"id","in":"path"},{"schema":{"type":"string","description":"The household member's user ID"},"required":true,"description":"The household member's user ID","name":"user_id","in":"path"}],"responses":{"204":{"description":"Relation cleared successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/preferences":{"get":{"security":[{"bearerAuth":[]}],"tags":["preferences"],"summary":"Get the household's preferences","description":"Returns, in one read, what the authenticated user's household as a whole plans around, what each member eats and won't, and the same for each person the household feeds who isn't a member. Read it before planning or shopping for anyone, so diets, dislikes and timing are respected. Everyone is listed, with null preferences when nobody has said.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"household":{"type":"string","nullable":true,"description":"What the household as a whole plans around, like \"no onions near the dog\" or \"we eat late on weeknights\". Null when nobody has said."},"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["user_id","name","preferences"]},"description":"Every household member, with what they eat and won't."},"people":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["id","name","preferences"]},"description":"Every person the household feeds who isn't a member, archived ones left out. Change theirs with PUT /rest/people/{id}."}},"required":["household","members","people"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/preferences/household":{"put":{"security":[{"bearerAuth":[]}],"tags":["preferences"],"summary":"Set the household's own preferences","description":"Replaces what the household as a whole plans around — \"no onions near the dog\", \"we eat late on weeknights\". It is one free-text field, so to add a fact, read it from GET /rest/preferences and send the combined text. Answers with the household's preferences as GET /rest/preferences does.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"string","nullable":true,"maxLength":2000,"description":"The whole text, replacing what was there. Null or blank clears it."}},"required":["preferences"]}}}},"responses":{"200":{"description":"Preferences updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"household":{"type":"string","nullable":true,"description":"What the household as a whole plans around, like \"no onions near the dog\" or \"we eat late on weeknights\". Null when nobody has said."},"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["user_id","name","preferences"]},"description":"Every household member, with what they eat and won't."},"people":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["id","name","preferences"]},"description":"Every person the household feeds who isn't a member, archived ones left out. Change theirs with PUT /rest/people/{id}."}},"required":["household","members","people"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/preferences/members/{user_id}":{"put":{"security":[{"bearerAuth":[]}],"tags":["preferences"],"summary":"Set one household member's preferences","description":"Replaces what one member of the household eats and won't. Any member may set any member's, since the household plans meals for everyone in it. It is one free-text field, so to add a fact, read it from GET /rest/preferences and send the combined text. Answers with the household's preferences as GET /rest/preferences does. For people the household feeds who aren't members, use PUT /rest/people/{id}.","parameters":[{"schema":{"type":"string","description":"The household member's user ID"},"required":true,"description":"The household member's user ID","name":"user_id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"string","nullable":true,"maxLength":2000,"description":"The whole text, replacing what was there. Null or blank clears it."}},"required":["preferences"]}}}},"responses":{"200":{"description":"Preferences updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"household":{"type":"string","nullable":true,"description":"What the household as a whole plans around, like \"no onions near the dog\" or \"we eat late on weeknights\". Null when nobody has said."},"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["user_id","name","preferences"]},"description":"Every household member, with what they eat and won't."},"people":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"preferences":{"type":"string","nullable":true,"description":"In the household's own words. Null when nobody has said."}},"required":["id","name","preferences"]},"description":"Every person the household feeds who isn't a member, archived ones left out. Change theirs with PUT /rest/people/{id}."}},"required":["household","members","people"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/pantry-items":{"get":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"List or search the household's pantry items","description":"Returns the things the authenticated user's household keeps and restocks, fridge and freezer included, food or not, in the order they were added. With `q`, returns instead the pantry items whose names best match it, best match first and at most 10 — search before creating a pantry item, so \"fresh parsley\" finds the parsley already there rather than becoming a second one. `staples_only` keeps only what the household always has on hand, with or without `q`.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Search the pantry items' names for this text."},"required":false,"description":"Search the pantry items' names for this text.","name":"q","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Only pantry items kept on hand. Defaults to false."},"required":false,"description":"Only pantry items kept on hand. Defaults to false.","name":"staples_only","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"description":"The household's own take on it. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","is_staple","notes","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"Add one of the household's pantry items","description":"Adds something the authenticated user's household keeps and restocks, fridge and freezer included, food or not: parsley, milk, wine, sponges. Nothing stops a second pantry item with the same name, so search first with GET /rest/pantry-items?q= and reuse a match: \"fresh parsley\" and \"parsley\" should be one pantry item. Facts anyone could look up, like how long it keeps or where it's stored, don't belong in `notes`; what this household knows about it does.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"parsley\" or \"dish soap\"."},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand. Defaults to false."},"notes":{"type":"string","maxLength":1000,"description":"The household's own take on it, like \"Diamond Crystal, not Morton\". Blank is stored as no notes."}},"required":["name"]}}}},"responses":{"201":{"description":"Pantry item created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"description":"The household's own take on it. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","is_staple","notes","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/pantry-items/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"Get one of the household's pantry items","description":"Returns one thing the household keeps and restocks, with whether it's a staple and the household's notes on it.","parameters":[{"schema":{"type":"string","description":"Pantry item ID"},"required":true,"description":"Pantry item ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"description":"The household's own take on it. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","is_staple","notes","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"Update one of the household's pantry items","description":"Renames a pantry item, marks it a staple or not, or changes its notes. Every field is optional and an omitted one is left alone.","parameters":[{"schema":{"type":"string","description":"Pantry item ID"},"required":true,"description":"Pantry item ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"parsley\" or \"dish soap\"."},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"maxLength":1000,"description":"The household's own take on it. Send null to clear it."}}}}}},"responses":{"200":{"description":"Pantry item updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"description":"The household's own take on it. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","is_staple","notes","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"Delete one of the household's pantry items","description":"Deletes a pantry item along with what the household noted about it on hand. Purchase lines and shopping list items that named it stay, no longer linked to a pantry item, and still say in their own words what was bought or wanted.","parameters":[{"schema":{"type":"string","description":"Pantry item ID"},"required":true,"description":"Pantry item ID","name":"id","in":"path"}],"responses":{"204":{"description":"Pantry item deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/services":{"get":{"security":[{"bearerAuth":[]}],"tags":["services"],"summary":"List or search the household's services","description":"Returns the companies that get food to the authenticated user's household without it going anywhere — a grocery delivery app, a meal kit, a restaurant delivery app — in the order they were added. With `q`, returns instead the services whose names best match it, best match first and at most 10 — search before creating a service, so the same company isn't added twice.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Search the services' names for this text."},"required":false,"description":"Search the services' names for this text.","name":"q","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["services"],"summary":"Add one of the household's services","description":"Adds a company that gets food to the authenticated user's household without it going anywhere: a grocery delivery app, a meal kit, a restaurant delivery app. Nothing stops a second service with the same name, so search first with GET /rest/services?q= and reuse a match.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"Speedy Basket\"."}},"required":["name"]}}}},"responses":{"201":{"description":"Service created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/services/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["services"],"summary":"Get one of the household's services","description":"Returns one company that gets food to the household without it going anywhere.","parameters":[{"schema":{"type":"string","description":"Service ID"},"required":true,"description":"Service ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["services"],"summary":"Update one of the household's services","description":"Renames one of the household's services.","parameters":[{"schema":{"type":"string","description":"Service ID"},"required":true,"description":"Service ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"Speedy Basket\"."}},"required":["name"]}}}},"responses":{"200":{"description":"Service updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["services"],"summary":"Delete one of the household's services","description":"Deletes a service. Purchases it delivered and shopping list items that named it stay, no longer linked to a service.","parameters":[{"schema":{"type":"string","description":"Service ID"},"required":true,"description":"Service ID","name":"id","in":"path"}],"responses":{"204":{"description":"Service deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/purchases":{"get":{"security":[{"bearerAuth":[]}],"tags":["purchases"],"summary":"List the household's purchases","description":"Returns the times the authenticated user's household bought something, each with its lines, newest day first and the most recently logged first within a day. Returns up to `limit` purchases (default 20, at most 100), plus the rest of the last day's so a day is never split. For the next page, pass the last purchase's `on` as `before`.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"How many purchases to return. Defaults to 20."},"required":false,"description":"How many purchases to return. Defaults to 20.","name":"limit","in":"query"},{"schema":{"type":"string","description":"Only purchases from days before this one, as YYYY-MM-DD."},"required":false,"description":"Only purchases from days before this one, as YYYY-MM-DD.","name":"before","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"on":{"type":"string","description":"The day it was bought, as YYYY-MM-DD."},"total":{"type":"integer","nullable":true,"minimum":0,"description":"What was paid, in minor units (cents). Null when not recorded."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code for every amount on the purchase."},"external_ref":{"type":"string","nullable":true,"description":"The order number or email id the purchase was read from."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The company that brought it. Null for a trip of the household's own."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The place it was bought. Null when nobody said."},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string","description":"The line as written on the receipt."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item this line was. Null when nobody matched one."},"quantity":{"type":"string","nullable":true,"description":"Free text as the receipt says it, like \"2 @ 1.25\"."},"price":{"type":"integer","nullable":true,"minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["id","name","pantry_item_id","quantity","price"]}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","on","total","currency","external_ref","service_id","place_id","lines","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["purchases"],"summary":"Log one of the household's purchases","description":"Logs one time the authenticated user's household bought something: a grocery run, a takeout order, a delivery. Lines are written as the receipt printed them. Amounts are in minor units (cents) and need a `currency` whenever there is one; a line's `price` is what the line cost, never a unit price. Link a line to a pantry item only when one matches.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"on":{"type":"string","description":"The day it was bought, as an ISO 8601 calendar date (YYYY-MM-DD). Dates have no time zone."},"total":{"type":"integer","minimum":0,"description":"What was paid, in minor units (cents), including tax, tips and fees."},"currency":{"type":"string","pattern":"^[A-Za-z]{3}$","description":"ISO 4217 code for the total and every line's price, like USD. Required whenever the purchase has a total or any line has a price."},"external_ref":{"type":"string","maxLength":200,"description":"The order number or email id the purchase was read from, so a second reading of the same confirmation can be caught."},"service_id":{"type":"string","minLength":1,"description":"The company that brought it, like a grocery delivery app or a meal kit. Absent for a trip the household made itself."},"place_id":{"type":"string","minLength":1,"description":"The place it was bought, from GET /rest/places."},"lines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The line as written on the receipt, like \"PARSLEY ITAL BNCH\"."},"pantry_item_id":{"type":"string","minLength":1,"description":"The household's pantry item this line was, when one matches. Leave it out rather than creating a pantry item for every line."},"quantity":{"type":"string","maxLength":100,"description":"Free text as the receipt says it, like \"2 @ 1.25\" or \"0.42 lb\"."},"price":{"type":"integer","minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["name"]},"maxItems":300,"description":"The purchase's lines, in the order the receipt printed them. On an update this is the whole list, not an addition to it."}},"required":["on"]}}}},"responses":{"201":{"description":"Purchase logged successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"on":{"type":"string","description":"The day it was bought, as YYYY-MM-DD."},"total":{"type":"integer","nullable":true,"minimum":0,"description":"What was paid, in minor units (cents). Null when not recorded."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code for every amount on the purchase."},"external_ref":{"type":"string","nullable":true,"description":"The order number or email id the purchase was read from."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The company that brought it. Null for a trip of the household's own."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The place it was bought. Null when nobody said."},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string","description":"The line as written on the receipt."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item this line was. Null when nobody matched one."},"quantity":{"type":"string","nullable":true,"description":"Free text as the receipt says it, like \"2 @ 1.25\"."},"price":{"type":"integer","nullable":true,"minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["id","name","pantry_item_id","quantity","price"]}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","on","total","currency","external_ref","service_id","place_id","lines","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/purchases/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["purchases"],"summary":"Get one of the household's purchases","description":"Returns one time the household bought something, with its lines as written on the receipt.","parameters":[{"schema":{"type":"string","description":"Purchase ID"},"required":true,"description":"Purchase ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"on":{"type":"string","description":"The day it was bought, as YYYY-MM-DD."},"total":{"type":"integer","nullable":true,"minimum":0,"description":"What was paid, in minor units (cents). Null when not recorded."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code for every amount on the purchase."},"external_ref":{"type":"string","nullable":true,"description":"The order number or email id the purchase was read from."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The company that brought it. Null for a trip of the household's own."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The place it was bought. Null when nobody said."},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string","description":"The line as written on the receipt."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item this line was. Null when nobody matched one."},"quantity":{"type":"string","nullable":true,"description":"Free text as the receipt says it, like \"2 @ 1.25\"."},"price":{"type":"integer","nullable":true,"minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["id","name","pantry_item_id","quantity","price"]}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","on","total","currency","external_ref","service_id","place_id","lines","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["purchases"],"summary":"Update one of the household's purchases","description":"Changes a purchase. Every field is optional and an omitted one is left alone; send null to clear `total`, `currency`, `external_ref`, `service_id` or `place_id`. `lines` replaces the whole list.","parameters":[{"schema":{"type":"string","description":"Purchase ID"},"required":true,"description":"Purchase ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"on":{"type":"string","description":"The day it was bought, as an ISO 8601 calendar date (YYYY-MM-DD). Dates have no time zone."},"total":{"type":"integer","nullable":true,"minimum":0,"description":"What was paid, in minor units (cents). Send null to clear it."},"currency":{"type":"string","nullable":true,"pattern":"^[A-Za-z]{3}$","description":"ISO 4217 code for the total and every line's price, like USD. Required whenever the purchase has a total or any line has a price."},"external_ref":{"type":"string","nullable":true,"maxLength":200,"description":"The order number or email id the purchase was read from, so a second reading of the same confirmation can be caught."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The company that brought it, like a grocery delivery app or a meal kit. Absent for a trip the household made itself."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The place it was bought. Send null to clear it."},"lines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The line as written on the receipt, like \"PARSLEY ITAL BNCH\"."},"pantry_item_id":{"type":"string","minLength":1,"description":"The household's pantry item this line was, when one matches. Leave it out rather than creating a pantry item for every line."},"quantity":{"type":"string","maxLength":100,"description":"Free text as the receipt says it, like \"2 @ 1.25\" or \"0.42 lb\"."},"price":{"type":"integer","minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["name"]},"maxItems":300,"description":"The purchase's lines, in the order the receipt printed them. On an update this is the whole list, not an addition to it."}}}}}},"responses":{"200":{"description":"Purchase updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"on":{"type":"string","description":"The day it was bought, as YYYY-MM-DD."},"total":{"type":"integer","nullable":true,"minimum":0,"description":"What was paid, in minor units (cents). Null when not recorded."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code for every amount on the purchase."},"external_ref":{"type":"string","nullable":true,"description":"The order number or email id the purchase was read from."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The company that brought it. Null for a trip of the household's own."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The place it was bought. Null when nobody said."},"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string","description":"The line as written on the receipt."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item this line was. Null when nobody matched one."},"quantity":{"type":"string","nullable":true,"description":"Free text as the receipt says it, like \"2 @ 1.25\"."},"price":{"type":"integer","nullable":true,"minimum":0,"description":"What the line cost as printed, in minor units (cents): the line's total, never a unit price."}},"required":["id","name","pantry_item_id","quantity","price"]}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","on","total","currency","external_ref","service_id","place_id","lines","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["purchases"],"summary":"Delete one of the household's purchases","description":"Deletes a purchase along with its lines. The pantry items its lines named are untouched.","parameters":[{"schema":{"type":"string","description":"Purchase ID"},"required":true,"description":"Purchase ID","name":"id","in":"path"}],"responses":{"204":{"description":"Purchase deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/pantry-items/{id}/history":{"get":{"security":[{"bearerAuth":[]}],"tags":["pantry items"],"summary":"Get a pantry item's recent purchases and pantry notes","description":"Returns one of the household's pantry items with the latest 10 purchase lines that bought it and its latest 10 pantry notes, each newest first. Nothing records what is on hand: read these together to judge whether the household probably still has it, and treat the answer as a guess.","parameters":[{"schema":{"type":"string","description":"Pantry item ID"},"required":true,"description":"Pantry item ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"pantry_item":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"is_staple":{"type":"boolean","description":"Whether the household always keeps it on hand."},"notes":{"type":"string","nullable":true,"description":"The household's own take on it. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","is_staple","notes","created_at","updated_at"]},"purchase_lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"purchase_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"on":{"type":"string","description":"The day the purchase happened, as YYYY-MM-DD."},"name":{"type":"string","description":"The line as written on the receipt."},"quantity":{"type":"string","nullable":true,"description":"Free text as the receipt says it."},"price":{"type":"integer","nullable":true,"minimum":0,"description":"What the line cost, in minor units (cents)."},"currency":{"type":"string","nullable":true,"description":"ISO 4217 code for the price."},"service_name":{"type":"string","nullable":true,"description":"The company that brought it. Null for a trip of the household's own."}},"required":["id","purchase_id","on","name","quantity","price","currency","service_name"]},"description":"The latest lines that bought this pantry item, newest purchase day first."},"pantry_notes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"notes":{"type":"string","description":"What happened, in the household's words."},"on":{"type":"string","description":"The day it happened, as YYYY-MM-DD."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","pantry_item_id","author_id","notes","on","created_at","updated_at"]},"description":"The latest pantry notes about it, newest day first."}},"required":["pantry_item","purchase_lines","pantry_notes"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/pantry-notes":{"get":{"security":[{"bearerAuth":[]}],"tags":["pantry notes"],"summary":"List one pantry item's notes","description":"Returns what the household has said happened to one pantry item on hand, newest day first and the latest written first within a day, up to `limit` (default 20, at most 100). To judge whether it's still around, read `GET /rest/pantry-items/{id}/history` instead, which adds its recent purchases.","parameters":[{"schema":{"type":"string","description":"The pantry item whose notes to list."},"required":true,"description":"The pantry item whose notes to list.","name":"pantry_item_id","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"How many notes to return. Defaults to 20."},"required":false,"description":"How many notes to return. Defaults to 20.","name":"limit","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"notes":{"type":"string","description":"What happened, in the household's words."},"on":{"type":"string","description":"The day it happened, as YYYY-MM-DD."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","pantry_item_id","author_id","notes","on","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["pantry notes"],"summary":"Record a pantry note","description":"Records something that happened to one of the household's pantry items on hand, in the household's words: \"put it in the freezer\", \"still good\", \"tossed it\". Nothing stores what is on hand, so a correction is another note rather than an edit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"notes":{"type":"string","minLength":1,"maxLength":1000,"description":"What happened to it, in the household's words, like \"put it in the freezer\" or \"tossed it\"."},"on":{"type":"string","description":"The day it happened, as an ISO 8601 calendar date (YYYY-MM-DD). Dates have no time zone."}},"required":["pantry_item_id","notes","on"]}}}},"responses":{"201":{"description":"Pantry note recorded successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"notes":{"type":"string","description":"What happened, in the household's words."},"on":{"type":"string","description":"The day it happened, as YYYY-MM-DD."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","pantry_item_id","author_id","notes","on","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/pantry-notes/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["pantry notes"],"summary":"Get one of the household's pantry notes","description":"Returns one thing the household said happened to food on hand, in its own words.","parameters":[{"schema":{"type":"string","description":"Pantry note ID"},"required":true,"description":"Pantry note ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"notes":{"type":"string","description":"What happened, in the household's words."},"on":{"type":"string","description":"The day it happened, as YYYY-MM-DD."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","pantry_item_id","author_id","notes","on","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["pantry notes"],"summary":"Update one of the household's pantry notes","description":"Rewords a pantry note or moves it to another day. Every field is optional and an omitted one is left alone. When what it said turned out wrong, record another note instead.","parameters":[{"schema":{"type":"string","description":"Pantry note ID"},"required":true,"description":"Pantry note ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"notes":{"type":"string","minLength":1,"maxLength":1000,"description":"What happened to it, in the household's words, like \"put it in the freezer\" or \"tossed it\"."},"on":{"type":"string","description":"The day it happened, as an ISO 8601 calendar date (YYYY-MM-DD). Dates have no time zone."}}}}}},"responses":{"200":{"description":"Pantry note updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it's about."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"notes":{"type":"string","description":"What happened, in the household's words."},"on":{"type":"string","description":"The day it happened, as YYYY-MM-DD."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","pantry_item_id","author_id","notes","on","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["pantry notes"],"summary":"Delete one of the household's pantry notes","description":"Deletes a pantry note recorded by mistake. The pantry item it was about is untouched.","parameters":[{"schema":{"type":"string","description":"Pantry note ID"},"required":true,"description":"Pantry note ID","name":"id","in":"path"}],"responses":{"204":{"description":"Pantry note deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/shopping-list":{"get":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"List the household's shopping list","description":"Returns everything on the authenticated user's household's shopping list, in the order it was added, each with its pantry item's name and the meals it's for. Checked items are included unless `include_checked` is false.","parameters":[{"schema":{"type":"string","enum":["true","false"],"description":"Whether to include checked items. Defaults to true."},"required":false,"description":"Whether to include checked items. Defaults to true.","name":"include_checked","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Add an item to the shopping list","description":"Puts one thing on the household's shopping list. Without `pantry_item_id`, it links to the household's pantry item with the same name, ignoring case, and creates that pantry item when there's none, so adding something new creates its pantry item once. The item is added exactly as written, and the response is that item. Shortly after, an amount in the name, as in \"2 lemons\", is read as the quantity, and something already on the list and not checked off is joined rather than duplicated: \"2 lemons\" added to \"1 lemon\" makes one item, \"3 lemons\", so the item added may be merged away, keeping the `note` already on the list or taking the added one's when there is none. Its `section` is filled in at the same time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What to buy, in the words of whoever added it, like \"parsley\"."},"quantity":{"type":"string","maxLength":100,"description":"How much, as free text typed beside the name, like \"a bunch\" or \"2 lb\". It's read into `count` and `unit` and kept in the words of the item's line, not stored as text. Blank is no quantity."},"count":{"type":"number","minimum":0,"exclusiveMinimum":true,"maximum":10000,"description":"How many to buy, of the unit when there is one."},"unit":{"type":"string","minLength":1,"maxLength":100,"description":"What the count counts, in any spelling: \"tbsp\", \"Tbsp.\", \"cans\". Stored normalized. Omit it when the count is of the thing itself, as in 3 onions."},"note":{"type":"string","maxLength":500,"description":"A note on what to buy, as free text, like \"boneless\" or \"0% or 2%\". Blank is stored as no note."},"meal_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":20,"description":"The household's meals it's for, if any."},"pantry_item_id":{"type":"string","minLength":1,"description":"The pantry item it is. Omit it to link the household's pantry item with the same name, ignoring case, creating one when there's none."},"place_id":{"type":"string","minLength":1,"description":"The place to buy it, from GET /rest/places."}},"required":["name"]}}}},"responses":{"201":{"description":"Item added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/shopping-list/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Get one item on the household's shopping list","description":"Returns one shopping list item with its pantry item's name and the meals it's for.","parameters":[{"schema":{"type":"string","description":"Shopping list item ID"},"required":true,"description":"Shopping list item ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Update an item on the shopping list","description":"Rewords an item, or changes its amount (`count` and `unit`), its note, its store section, which meals it's for, which pantry item it is, where to buy it, or who is buying it. Every field is optional and an omitted one is left alone; `meal_ids` replaces the meals whole, and a null or blank `note` clears it. Renaming leaves the pantry item link alone. A new `section` is remembered on the item's pantry item, so the next time the same thing is added to the list it goes there instead of where it would otherwise be filed; other items already on the list stay where they are. A `unit` of the household's own, like \"tub\", is remembered there too, in `pantry_item_units`. The store is one of the household's places (`place_id`), one of its delivery services (`service_id`), or both. Check items off with `POST /rest/shopping-list/{id}/check`.","parameters":[{"schema":{"type":"string","description":"Shopping list item ID"},"required":true,"description":"Shopping list item ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What to buy, in the words of whoever added it, like \"parsley\"."},"quantity":{"description":"Removed. Send `count` and `unit` instead; a request that sends it is rejected."},"count":{"type":"number","nullable":true,"minimum":0,"exclusiveMinimum":true,"maximum":10000,"description":"How many to buy. Keeps the unit, and null clears the count but keeps the unit."},"unit":{"type":"string","nullable":true,"minLength":1,"maxLength":100,"description":"What the count counts, in any spelling, like \"tbsp\" or \"cans\". Keeps the count, and null makes it a count of the thing itself."},"note":{"type":"string","nullable":true,"maxLength":500,"description":"A note on what to buy, like \"boneless\". Send null or a blank to clear it."},"section":{"type":"string","enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other"],"description":"The store section to file it under: \"produce\" (Produce), \"bakery\" (Bakery), \"meat\" (Meat), \"seafood\" (Seafood), \"dairy\" (Dairy), \"frozen\" (Frozen), \"grains\" (Pasta and grains), \"canned\" (Canned and jarred), \"oils\" (Oils and vinegars), \"condiments\" (Condiments), \"nuts\" (Nuts and seeds), \"spices\" (Spices), \"baking\" (Baking), \"snacks\" (Snacks), \"beverages\" (Beverages), \"household\" (Household), \"other\" (Other). Remembered on its pantry item, so the next time the same thing is added to the list it goes there too."},"meal_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":20,"description":"The meals it's for, replacing the ones it was for."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Send null to unlink it."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Send null to clear it."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Send null to clear it."},"buyer_user_id":{"type":"string","nullable":true,"minLength":1,"description":"The household member buying it, by user ID from GET /rest/household. Send null to clear it."}}}}}},"responses":{"200":{"description":"Item updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Remove an item from the shopping list","description":"Takes one item off the household's shopping list. Its pantry item is untouched.","parameters":[{"schema":{"type":"string","description":"Shopping list item ID"},"required":true,"description":"Shopping list item ID","name":"id","in":"path"}],"responses":{"204":{"description":"Item removed successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/shopping-list/{id}/check":{"post":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Check an item off the shopping list","description":"Checks an item off as the authenticated user, now. Checking one already checked keeps it checked and records this check instead, since only the latest is kept.","parameters":[{"schema":{"type":"string","description":"Shopping list item ID"},"required":true,"description":"Shopping list item ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Item checked off","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/shopping-list/{id}/uncheck":{"post":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Uncheck an item on the shopping list","description":"Puts a checked item back on the list to buy, forgetting who checked it. Unchecking one that isn't checked changes nothing.","parameters":[{"schema":{"type":"string","description":"Shopping list item ID"},"required":true,"description":"Shopping list item ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Item unchecked","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/shopping-list/clear-checked":{"post":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Clear checked items from the shopping list","description":"Deletes every checked item on the household's shopping list, once the shop is done, and says how many went. Their pantry items are untouched.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Checked items cleared","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer","minimum":0,"description":"How many checked items were deleted."}},"required":["deleted"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/recipes/{id}/shopping-list":{"post":{"security":[{"bearerAuth":[]}],"tags":["shopping list"],"summary":"Add a recipe to the shopping list","description":"Puts everything a recipe needs on the household's shopping list, the same way the app's add-to-groceries sheet does: a plan reads the recipe's ingredient lines, drops prep and sizes, joins what is already on the list and not checked off (\"2 lemons\" onto \"1 lemon\" makes \"3 lemons\"), and files each item under a store section. If the plan can't be made, the rules' reading of the lines is written instead, so this does not fail because of the model; `source` says which was used. Returns the rows written, new items and existing ones it joined.","parameters":[{"schema":{"type":"string","description":"Recipe ID"},"required":true,"description":"Recipe ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Recipe added","content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string","enum":["plan","rules"],"description":"How the recipe was read. \"plan\" is the AI plan the app's sheet uses, which merges with the list and fills in store sections. \"rules\" is the fallback reading used when the plan could not be made."},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string","description":"What to buy, in the words of whoever added it."},"note":{"type":"string","nullable":true,"description":"A note on what to buy, like \"boneless\" or \"0% or 2%\". Null when there is none."},"quantity":{"type":"string","nullable":true,"description":"How much, as the list shows it, written from `count` and `unit`: \"2 cans\", \"1½ cups\". Null when there is no count."},"count":{"type":"number","nullable":true,"description":"How many to buy, of `unit` when there is one. Null when the amount has no one number, like \"a pinch\", when its lines ask for units that don't add, like \"1 stick\" and \"4 tbsp.\", or when nobody said."},"unit":{"type":"string","nullable":true,"description":"What the count counts: a measured unit (g, kg, ml, l, tsp, tbsp, cup_us, cup_metric, oz, floz, lb, pint, quart, gallon) or the household's own word, lowercase and singular, like \"can\" or \"bunch\". Null when the count is of the thing itself."},"section":{"type":"string","nullable":true,"enum":["produce","bakery","meat","seafood","dairy","frozen","grains","canned","oils","condiments","nuts","spices","baking","snacks","beverages","household","other",null],"description":"The store section it's filed under. Null when it has none; one is filled in shortly after it's added, when one can be."},"pantry_item_id":{"type":"string","nullable":true,"minLength":1,"description":"The pantry item it is. Null when it isn't linked to one."},"pantry_item_name":{"type":"string","nullable":true,"description":"The linked pantry item's name. Null when it isn't linked to one."},"pantry_item_units":{"type":"array","items":{"type":"string"},"description":"Units of the household's own that someone has used for the linked pantry item, like \"tub\" or \"block\", lowercase and singular, in the order first used. Empty when there are none or it isn't linked to one."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"The store to buy it at, from GET /rest/places. Null when nobody said."},"service_id":{"type":"string","nullable":true,"minLength":1,"description":"The delivery service bringing it, from GET /rest/services. Can be set alongside `place_id`. Null when nobody said."},"buyer":{"type":"object","nullable":true,"properties":{"id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["id","name"],"description":"The household member buying it. Null when nobody said, or once they leave the household."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"source_lines":{"type":"array","items":{"type":"object","properties":{"line":{"type":"string","description":"The words it was added in: a recipe's line, like \"2 cans chickpeas, drained\", or what someone typed, like \"a bunch Parsley\"."},"count":{"type":"number","nullable":true,"description":"This line's own amount, as asked for and never converted. Null when it gave no number, or before it has been read."},"unit":{"type":"string","nullable":true,"description":"What this line's count counts, in the form of `unit`."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it came from. Null for a typed line. A deleted recipe's lines drop off."},"added_by":{"type":"string","nullable":true,"minLength":1,"description":"Who added it. Null when that wasn't recorded, or once that person has deleted their account."},"added_at":{"type":"string","nullable":true,"format":"date-time","description":"When it was added. Null when that wasn't recorded."}},"required":["line","count","unit","recipe_id","added_by","added_at"]},"description":"Each time it was added, oldest first, in the words it came in, with that line's own amount."},"checked":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was checked off."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who checked it off. Null once that person has deleted their account."}},"required":["at","by"],"description":"The latest check. Null while it's still to buy."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","note","quantity","count","unit","section","pantry_item_id","pantry_item_name","pantry_item_units","place_id","service_id","buyer","meals","source_lines","checked","created_at","updated_at"]},"description":"The rows the add wrote: new items and existing ones it joined. Empty when the recipe has no ingredients."}},"required":["source","items"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/tasks":{"get":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"List the household's tasks","description":"Returns the authenticated user's household's open tasks: those with no due date first, then by due date, then in the order they were added. Each comes with the meals it's for, who is doing it, and its recipe's title. `meal_id` narrows to one meal's tasks. `from` and `to` narrow to tasks due between them, inclusive; either alone is open on the other side, and either leaves out tasks with no due date.\n\nDone tasks are left out unless `include_done` is true. Then the most recently done follow the open ones, newest done first: up to `limit` of them (default 50, at most 200), counted before `from` and `to` narrow them.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Only the tasks for this meal."},"required":false,"description":"Only the tasks for this meal.","name":"meal_id","in":"query"},{"schema":{"type":"string","description":"First due date to include, as YYYY-MM-DD. Inclusive."},"required":false,"description":"First due date to include, as YYYY-MM-DD. Inclusive.","name":"from","in":"query"},{"schema":{"type":"string","description":"Last due date to include, as YYYY-MM-DD. Inclusive."},"required":false,"description":"Last due date to include, as YYYY-MM-DD. Inclusive.","name":"to","in":"query"},{"schema":{"type":"string","enum":["true","false"],"description":"Whether to include done tasks. Defaults to false."},"required":false,"description":"Whether to include done tasks. Defaults to false.","name":"include_done","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200,"description":"How many of the most recently done tasks to include with `include_done`. Defaults to 50."},"required":false,"description":"How many of the most recently done tasks to include with `include_done`. Defaults to 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Add a task","description":"Adds one task: work that goes into feeding the household, like salting the chicken or picking up an avocado. Link it to the meals it's for. Its meals, recipe, place and assignee must be the household's, a member assignee must still be a member, and a person assignee mustn't be archived. A due time needs a due date.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"What to do, in the words of whoever added it, like \"Salt the chicken\"."},"due":{"type":"string","description":"The day it's due, as an ISO 8601 calendar date (YYYY-MM-DD). Dates have no time zone."},"due_time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"The local time it's due that day, as HH:MM on a 24-hour clock with no time zone: \"13:00\" means one in the afternoon wherever the household is. Needs `due`."},"assignee":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID, from GET /rest/household."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false}],"description":"Who is doing it: a member as `{user_id}`, or one of the household's people as `{person_id}`. Omit it for nobody."},"recipe_id":{"type":"string","minLength":1,"description":"The recipe it's part of, from GET /rest/recipes."},"place_id":{"type":"string","minLength":1,"description":"Where it happens, from GET /rest/places."},"meal_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":20,"description":"The household's meals it's for, if any."}},"required":["title"]}}}},"responses":{"201":{"description":"Task added successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/tasks/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Get one of the household's tasks","description":"Returns one task with the meals it's for, who is doing it, and its recipe's title.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Update a task","description":"Rewords a task, or changes when it's due, its recipe, where it happens, or which meals it's for. Every field is optional and an omitted one is left alone; `meal_ids` replaces the meals whole. A due time needs a due date, judged on the task after the change. Change who does it with `POST /rest/tasks/{id}/assign`, and mark it done with `POST /rest/tasks/{id}/finish`.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"What to do, in the words of whoever added it, like \"Salt the chicken\"."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Send null to clear it."},"due_time":{"type":"string","nullable":true,"pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"The local time it's due that day, as HH:MM. Send null to clear it."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Send null to clear it."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Send null to clear it."},"meal_ids":{"type":"array","items":{"type":"string","minLength":1,"description":"A Convex document ID."},"maxItems":20,"description":"The meals it's for, replacing the ones it was for."}}}}}},"responses":{"200":{"description":"Task updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Delete a task","description":"Deletes one of the household's tasks. The meals it was for are untouched.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"responses":{"204":{"description":"Task deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/tasks/{id}/assign":{"post":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Assign a task","description":"Gives a task to a household member (`{user_id}`) or one of the household's people (`{person_id}`), replacing whoever had it. Send `{}` to leave it with nobody. A member must still be in the household, and an archived person can't be given a task.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"A household member's user ID, from GET /rest/household."}},"required":["user_id"],"additionalProperties":false},{"type":"object","properties":{"person_id":{"type":"string","minLength":1,"description":"The ID of a person the household feeds who isn't a member, from GET /rest/people."}},"required":["person_id"],"additionalProperties":false},{"type":"object","properties":{},"additionalProperties":false}],"description":"Who should do it: `{user_id}` for a member, `{person_id}` for one of the household's people, or `{}` for nobody."}}}},"responses":{"200":{"description":"Task assigned","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/tasks/{id}/finish":{"post":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Mark a task done","description":"Marks a task done as the authenticated user, now. Finishing one already done keeps it done and records this instead, since only the latest is kept.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Task marked done","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/tasks/{id}/reopen":{"post":{"security":[{"bearerAuth":[]}],"tags":["tasks"],"summary":"Reopen a task","description":"Puts a done task back to do, forgetting who finished it. Reopening one that isn't done changes nothing.","parameters":[{"schema":{"type":"string","description":"Task ID"},"required":true,"description":"Task ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{},"description":"Send an empty JSON object."}}}},"responses":{"200":{"description":"Task reopened","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"title":{"type":"string","description":"What to do, in the words of whoever added it."},"due":{"type":"string","nullable":true,"description":"The day it's due, as YYYY-MM-DD. Null when it has no day."},"due_time":{"type":"string","nullable":true,"description":"The local time it's due that day, as HH:MM. Null when it has no time."},"assignee":{"type":"object","nullable":true,"properties":{"kind":{"type":"string","enum":["member","person"],"description":"`member` for a household member, `person` for someone from GET /rest/people."},"id":{"type":"string","minLength":1,"description":"The member's user ID, or the person's ID."},"name":{"type":"string","nullable":true,"description":"Their name. Null for a member who never gave one."}},"required":["kind","id","name"],"description":"Who is doing it. Null when nobody is."},"done":{"type":"object","nullable":true,"properties":{"at":{"type":"string","format":"date-time","description":"When it was marked done."},"by":{"type":"string","nullable":true,"minLength":1,"description":"Who marked it done. Null once that person has deleted their account."}},"required":["at","by"],"description":"When it was done, and by whom. Null while it's still to do."},"recipe_id":{"type":"string","nullable":true,"minLength":1,"description":"The recipe it's part of. Null when it isn't part of one."},"recipe_title":{"type":"string","nullable":true,"description":"The recipe's title. Null when there's no recipe or no title."},"place_id":{"type":"string","nullable":true,"minLength":1,"description":"Where it happens. Null when nobody said."},"meals":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"title":{"type":"string","nullable":true,"description":"What the meal is called. Null when it has no title."},"when":{"type":"string","nullable":true,"description":"When the meal is: a calendar date as YYYY-MM-DD, or null for an unplanned meal."}},"required":["id","title","when"]},"description":"The meals it's for. A deleted meal drops off."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","title","due","due_time","assignee","done","recipe_id","recipe_title","place_id","meals","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/equipment":{"get":{"security":[{"bearerAuth":[]}],"tags":["equipment"],"summary":"List or search the household's equipment","description":"Returns the kitchen equipment the authenticated user's household has — a stand mixer, a rice cooker — in the order it was added. If it's listed, they have it. With `q`, returns instead the equipment whose names best match it, best match first and at most 10 — search before adding equipment, so the same thing isn't added twice.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Search the equipment's names for this text."},"required":false,"description":"Search the equipment's names for this text.","name":"q","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"notes":{"type":"string","nullable":true,"description":"What's worth knowing about the household's own. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","notes","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["equipment"],"summary":"Add a piece of the household's equipment","description":"Adds a piece of kitchen equipment the authenticated user's household has, like a stand mixer or a rice cooker. Nothing stops a second piece with the same name, so search first with GET /rest/equipment?q= and reuse a match.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"stand mixer\"."},"notes":{"type":"string","maxLength":1000,"description":"What's worth knowing about the household's own, like \"KitchenAid Artisan, struggles with stiff doughs\". Blank is stored as no notes."}},"required":["name"]}}}},"responses":{"201":{"description":"Equipment created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"notes":{"type":"string","nullable":true,"description":"What's worth knowing about the household's own. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","notes","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/equipment/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["equipment"],"summary":"Get one piece of the household's equipment","description":"Returns one piece of kitchen equipment the household has, with its notes.","parameters":[{"schema":{"type":"string","description":"Equipment ID"},"required":true,"description":"Equipment ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"notes":{"type":"string","nullable":true,"description":"What's worth knowing about the household's own. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","notes","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["equipment"],"summary":"Update a piece of the household's equipment","description":"Renames a piece of the household's equipment or changes its notes. Every field is optional and an omitted one is left alone.","parameters":[{"schema":{"type":"string","description":"Equipment ID"},"required":true,"description":"Equipment ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"stand mixer\"."},"notes":{"type":"string","nullable":true,"maxLength":1000,"description":"What's worth knowing about the household's own. Send null to clear it."}}}}}},"responses":{"200":{"description":"Equipment updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"notes":{"type":"string","nullable":true,"description":"What's worth knowing about the household's own. Null when nobody has said."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","notes","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["equipment"],"summary":"Delete a piece of the household's equipment","description":"Deletes a piece of equipment, for when the household gets rid of it or it was added by mistake.","parameters":[{"schema":{"type":"string","description":"Equipment ID"},"required":true,"description":"Equipment ID","name":"id","in":"path"}],"responses":{"204":{"description":"Equipment deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/places":{"get":{"security":[{"bearerAuth":[]}],"tags":["places"],"summary":"List or search the household's places","description":"Returns the places the authenticated user's household has recorded — somewhere food comes from or gets eaten, like a market, a restaurant or a relative's kitchen — in the order they were added. With `q`, returns instead the places whose names best match it, best match first and at most 10 — search before adding a place, so the same one isn't added twice.","parameters":[{"schema":{"type":"string","minLength":1,"description":"Search the places' names for this text."},"required":false,"description":"Search the places' names for this text.","name":"q","in":"query"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","type","created_at","updated_at"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"post":{"security":[{"bearerAuth":[]}],"tags":["places"],"summary":"Add a place","description":"Adds a place to the authenticated user's household: somewhere food comes from or gets eaten, like a market, a restaurant or a relative's kitchen. Set `type` to say which kind it is. Nothing stops a second place with the same name, so search first with GET /rest/places?q= and reuse a match.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"Corner bakery\"."},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item. Use \"other\" when it isn't a store or home."}},"required":["name","type"]}}}},"responses":{"201":{"description":"Place created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","type","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/places/{id}":{"get":{"security":[{"bearerAuth":[]}],"tags":["places"],"summary":"Get one of the household's places","description":"Returns one place the household has recorded.","parameters":[{"schema":{"type":"string","description":"Place ID"},"required":true,"description":"Place ID","name":"id","in":"path"}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","type","created_at","updated_at"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"put":{"security":[{"bearerAuth":[]}],"tags":["places"],"summary":"Update one of the household's places","description":"Renames a place or changes its type. Every field is optional and an omitted one is left alone. Purchases, shopping list items, meals and tasks that name the place follow the new name.","parameters":[{"schema":{"type":"string","description":"Place ID"},"required":true,"description":"Place ID","name":"id","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"What the household calls it, like \"Corner bakery\"."},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item. It can be changed but not cleared."}}}}}},"responses":{"200":{"description":"Place updated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"household_id":{"type":"string","minLength":1,"description":"A Convex document ID."},"author_id":{"type":"string","nullable":true,"minLength":1,"description":"Who created this. Null once that person has deleted their account."},"name":{"type":"string"},"type":{"type":"string","enum":["store","home","other"],"description":"What kind of location it is. \"store\" for somewhere groceries are bought, including a market or a store that delivers; \"home\" for the household's own home; \"other\" for anything else, such as a restaurant, a relative's kitchen or a workplace. Only stores are offered as somewhere to buy a grocery item."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}},"required":["id","household_id","author_id","name","type","created_at","updated_at"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}},"delete":{"security":[{"bearerAuth":[]}],"tags":["places"],"summary":"Delete one of the household's places","description":"Deletes a place. Purchases, shopping list items, meals and tasks that named it stay, with their place cleared.","parameters":[{"schema":{"type":"string","description":"Place ID"},"required":true,"description":"Place ID","name":"id","in":"path"}],"responses":{"204":{"description":"Place deleted successfully"},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/household":{"get":{"security":[{"bearerAuth":[]}],"tags":["households"],"summary":"Get the household","description":"Returns the household the authenticated user belongs to, with its members. A person belongs to exactly one household, and every other endpoint reads and writes inside it. Members' `user_id`s are what task assignees and preferences refer to.","responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","minLength":1,"description":"A Convex document ID."},"name":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","minLength":1,"description":"The member's user ID."},"name":{"type":"string","nullable":true}},"required":["user_id","name"]},"description":"Everyone who belongs to the household."}},"required":["id","created_at","updated_at","members"]}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}},"/rest/feedback":{"post":{"security":[{"bearerAuth":[]}],"tags":["feedback"],"summary":"Send feedback to the Al Dente team","description":"Submits a text message as feedback from the authenticated user. The calling app is responsible for confirming the message with the user before making this request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}}}},"responses":{"204":{"description":"Feedback submitted"},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"403":{"description":"Forbidden: the API key is read-only, or has no REST access","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"userFriendlyMessage":{"type":"string"},"error":{"type":"string"}}}}}}}}}}}