{"openapi":"3.1.0","info":{"title":"Flambe API","version":"1.0.0","summary":"Import, digitize and manage recipes programmatically.","description":"The Flambe API is the same HTTP interface the Flambe apps use. Everything the\nmobile app and web portal can do — importing a recipe from a URL, a photo of\na cookbook page or a block of pasted text, organizing a library, planning\nmeals, building grocery lists and sharing with a household — is available\nhere.\n\n## Base URL\n\n```\nhttps://api.flambe.dev\n```\n\nThere is no separate sandbox host. Use a dedicated test user for\nexperimentation.\n\n## Authentication\n\nEvery endpoint outside of `/health` requires a bearer token:\n\n```http\nAuthorization: Bearer <token>\n```\n\nTokens are [Clerk](https://clerk.com) session JWTs. See\n[Authentication](https://docs.flambe.dev/docs/guides/authentication) for how\nto obtain one.\n\n## Conventions\n\n- All request and response bodies are JSON, except file upload, which is\n  `multipart/form-data`.\n- Field names are `snake_case`. A handful of newer endpoints accept\n  `camelCase`; where that is true the schema says so explicitly.\n- Timestamps are ISO 8601 strings in UTC (`2026-09-10T18:22:04.512Z`).\n- Identifiers are UUID v4 strings. Do not parse them for meaning.\n- Errors return a non-2xx status and a body of `{ \"error\": \"message\" }`, or\n  `{ \"errors\": [...] }` when request validation failed.\n\n## Rate limits\n\nRequests are rate limited per IP. Exceeding the limit returns `429`. Treat\n`429` and `5xx` as retryable with exponential backoff; treat `4xx` as\nterminal.\n\n## Working with agents\n\nA plain-text summary of these docs for LLM context is published at\n[/llms.txt](https://docs.flambe.dev/llms.txt), the full corpus at\n[/llms-full.txt](https://docs.flambe.dev/llms-full.txt), and any page can be\nfetched as Markdown by appending `.md` to its URL.\n","license":{"name":"Proprietary","url":"https://flambe.dev/terms"},"contact":{"name":"Flambe Support","url":"https://flambe.dev/contact"}},"servers":[{"url":"https://api.flambe.dev","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Imports","description":"Turn a URL, image, or block of text into a structured recipe. This is\nFlambe's flagship capability and the most likely reason to reach for the\nAPI. Imports are asynchronous — see the\n[Imports guide](https://docs.flambe.dev/docs/guides/imports).\n"},{"name":"Recipes","description":"The recipe library — create, read, update, delete, and sync."},{"name":"Collections","description":"Named groupings of recipes."},{"name":"Meal plans","description":"Scheduled meals built from recipes."},{"name":"Grocery lists","description":"Shopping lists, optionally derived from meal plans."},{"name":"Wishlists","description":"Saved cookbooks and recipes a user wants but does not own yet."},{"name":"Cook sessions","description":"Live cooking sessions — which recipes are open right now."},{"name":"Timers","description":"Server-backed kitchen timers that survive app restarts."},{"name":"Files","description":"Upload and retrieve images and other binary assets."},{"name":"Households","description":"Multi-user sharing — members, invites, and the shared library."},{"name":"Sharing","description":"Public share links and the unauthenticated endpoints that resolve them."},{"name":"Sources","description":"Provenance — the cookbook, website, or creator a recipe came from."},{"name":"Creators","description":"Cookbook authors and social creators (v2 provenance model)."},{"name":"Notifications","description":"In-app notification feed and unread counts."},{"name":"Push tokens","description":"Device registration for push notifications."},{"name":"Books","description":"Cookbook lookup used when attributing a recipe to a book."},{"name":"AI","description":"Embedding and assistant primitives used by semantic search."},{"name":"Users","description":"The authenticated user's profile."},{"name":"Events","description":"Server-sent event stream for live updates."},{"name":"Auth","description":"Legacy username/password endpoints. New integrations should authenticate\nwith Clerk instead — see the Authentication guide.\n"},{"name":"System","description":"Health and service metadata."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"A Clerk session JWT. Obtain one with a Clerk frontend SDK\n(`session.getToken()`) or, for server-to-server use, with a Clerk\nmachine token. The token's `sub` claim is the Flambe user id that scopes\nevery request.\n"}},"parameters":{"RecipeId":{"name":"id","in":"path","required":true,"description":"Recipe identifier.","schema":{"type":"string","format":"uuid"}},"ImportId":{"name":"id","in":"path","required":true,"description":"Import identifier.","schema":{"type":"string","format":"uuid"}},"Limit":{"name":"limit","in":"query","required":false,"description":"Maximum items to return.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"Cursor":{"name":"cursor","in":"query","required":false,"description":"Opaque pagination cursor returned as `next_cursor` by the previous page.\nBase64-encoded DynamoDB `ExclusiveStartKey`; treat it as opaque.\n","schema":{"type":"string"}},"Since":{"name":"since","in":"query","required":false,"description":"Delta-sync watermark. Return only records changed at or after this\nISO 8601 timestamp.\n","schema":{"type":"string","format":"date-time"}}},"responses":{"BadRequest":{"description":"The request was malformed or failed validation.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/ValidationError"}]}}}},"Unauthorized":{"description":"No bearer token was supplied, or it was invalid or expired.\n\n`Access token required` means the `Authorization` header was absent\nentirely — a client bug, not an expired session. `Invalid or expired\ntoken` means the token was present but did not verify.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"missing":{"summary":"Header absent","value":{"error":"Access token required"}},"invalid":{"summary":"Token present but not valid","value":{"error":"Invalid or expired token"}}}}}},"Forbidden":{"description":"Authenticated, but not allowed to act on this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"No such resource, or it is not visible to this user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The resource is not in a state that permits this operation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Rate limit exceeded. Retry with exponential backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Unexpected server error. Safe to retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","description":"The standard error envelope.","required":["error"],"properties":{"error":{"type":"string","description":"A human-readable message. Not a stable machine identifier.","examples":["Recipe not found"]}}},"ValidationError":{"type":"object","description":"Returned when `express-validator` rejects the request body. Each entry\nidentifies the offending field.\n","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","examples":["field"]},"path":{"type":"string","description":"The rejected field.","examples":["title"]},"msg":{"type":"string","examples":["Invalid value"]},"location":{"type":"string","enum":["body","query","params","headers"]},"value":{"description":"The value that was rejected."}}}}}},"Ingredient":{"type":"object","description":"A single ingredient line, parsed into quantity, unit and name.","required":["name"],"properties":{"name":{"type":"string","description":"The ingredient itself, without quantity or preparation.","examples":["all-purpose flour"]},"quantity":{"type":"number","description":"Amount, as a decimal. Fractions are normalized (`½` becomes `0.5`).","examples":[2.5]},"unit":{"type":"string","description":"Unit for `quantity`, as written in the source.","examples":["cups"]},"preparation":{"type":"string","description":"How the ingredient is prepared before use.","examples":["sifted"]},"isOptional":{"type":"boolean","description":"Whether the recipe marks this ingredient as optional.","default":false}}},"Component":{"type":"object","description":"A sub-recipe within a recipe — a sauce, a dough, a streusel. Components\ncarry their own ingredients and instructions so a multi-part recipe does\nnot flatten into one ambiguous list.\n","required":["title","ingredients","instructions"],"properties":{"title":{"type":"string","examples":["Brown butter frosting"]},"description":{"type":"string"},"notes":{"type":"string"},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/Ingredient"}},"instructions":{"type":"array","items":{"type":"string"}}}},"Media":{"type":"object","description":"An image or video attached to a recipe.","properties":{"file_id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri","description":"CDN URL on `cdn.flambe.dev`. Width variants are available by\nappending `@{width}w.webp` for widths 320, 480, 640, 800, 960, 1200\nand 1600; any other width falls back to the original.\n"},"blur_hash":{"type":"string","description":"BlurHash placeholder to render while the image loads."},"width":{"type":"integer"},"height":{"type":"integer"},"kind":{"type":"string","enum":["image","video"]}}},"Recipe":{"type":"object","description":"A recipe in the authenticated user's library.","required":["title"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true,"description":"Owner. Always the caller for recipes created through this API."},"title":{"type":"string","examples":["Brown Butter Chocolate Chip Cookies"]},"subtitle":{"type":"string"},"description":{"type":"string"},"notes":{"type":"string","description":"Free-form user notes. Always a string; never null.","default":""},"tags":{"type":"array","items":{"type":"string"},"examples":[["dessert","cookies"]]},"ingredients":{"type":"array","description":"Top-level ingredients. When a recipe uses `components`, this list\nholds only the ingredients that belong to no component.\n","items":{"$ref":"#/components/schemas/Ingredient"}},"components":{"type":"array","items":{"$ref":"#/components/schemas/Component"}},"instructions":{"type":"array","items":{"type":"string"}},"media":{"type":"array","items":{"$ref":"#/components/schemas/Media"}},"blur_hash":{"type":"string"},"prep_time":{"type":"integer","description":"Minutes of active preparation.","examples":[20]},"cook_time":{"type":"integer","description":"Minutes of cooking.","examples":[12]},"servings":{"type":"integer","examples":[24]},"difficulty":{"type":"string","enum":["easy","medium","hard"]},"rating":{"type":"number","description":"The user's own rating, 0–5.","minimum":0,"maximum":5},"source_section":{"type":"string","description":"Where in the source this recipe appeared, e.g. a chapter or page."},"nutrition":{"$ref":"#/components/schemas/Nutrition"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Nutrition":{"type":"object","description":"Computed per-serving nutrition. Derived from ingredient matching against\nUSDA FoodData Central and Open Food Facts, so values are estimates.\n","properties":{"calories":{"type":"number"},"protein_g":{"type":"number"},"fat_g":{"type":"number"},"carbs_g":{"type":"number"},"fiber_g":{"type":"number"},"sugar_g":{"type":"number"},"sodium_mg":{"type":"number"},"per":{"type":"string","enum":["serving","recipe"]},"computed_at":{"type":"string","format":"date-time"},"ingredients_hash":{"type":"string","description":"Hash of the ingredient list the figures were computed from. If it no\nlonger matches the recipe, the nutrition is stale.\n"}}},"Import":{"type":"object","description":"An asynchronous recipe-extraction job. Poll it, or subscribe to\n`/api/events/stream`, until `status` is terminal.\n","required":["type"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"type":{"type":"string","enum":["web","media","text","image_batch"],"description":"- `web` — fetch and parse an https URL (recipe site, Instagram, YouTube).\n- `media` — read one or more uploaded images, e.g. cookbook pages.\n- `text` — parse pasted or uploaded plain text.\n- `image_batch` — a parent job that fans out into per-image children.\n"},"status":{"type":"string","enum":["pending","queued","processing","completed","failed","cancelled"],"description":"`pending` on creation, then `queued` once enqueued, `processing`\nwhile a worker holds it. `completed`, `failed` and `cancelled` are\nterminal.\n","readOnly":true},"url":{"type":"string","format":"uri","description":"Source URL, for `web` imports. Must be `https`."},"file_id":{"type":"string","description":"Deprecated single-file form of `file_ids`.","deprecated":true},"file_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Uploaded files to read, for `media` imports."},"text_file_id":{"type":"string","format":"uuid","description":"An uploaded text file to parse, as an alternative to `text`."},"source_url":{"type":"string","format":"uri","description":"Attribution URL for a `text` import. Must be `https`."},"source_section":{"type":"string","maxLength":120},"recipe_id":{"type":"string","format":"uuid","readOnly":true,"description":"The recipe this import produced. Present once `completed`."},"batch_id":{"type":"string","format":"uuid","description":"Groups children of an `image_batch` parent."},"error":{"type":"string","readOnly":true,"description":"Failure reason, present when `status` is `failed`."},"progress":{"type":"object","readOnly":true,"description":"Coarse progress for long-running imports.","properties":{"stage":{"type":"string","examples":["extracting"]},"percent":{"type":"integer","minimum":0,"maximum":100}}},"processing_started_at":{"type":"string","format":"date-time","readOnly":true,"description":"When the current attempt began. The retry budget is anchored here,\nnot to `created_at`, so a retry gets a full budget.\n"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Collection":{"type":"object","required":["name"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"name":{"type":"string","examples":["Weeknight dinners"]},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"recipes":{"type":"array","description":"Recipe ids in the collection.","items":{"type":"string","format":"uuid"}},"recipe_count":{"type":"integer","readOnly":true},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"MealPlan":{"type":"object","required":["name"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"name":{"type":"string","examples":["Week of Sep 14"]},"description":{"type":"string"},"end_date":{"type":"string","format":"date"},"meals":{"type":"array","items":{"$ref":"#/components/schemas/Meal"}},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Meal":{"type":"object","description":"A recurring slot in a meal plan — a time window on a set of weekdays,\nholding one or more items.\n","required":["start_time","end_time","days_of_week","items"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string","examples":["Dinner"]},"start_time":{"type":"string","examples":["18:00"]},"end_time":{"type":"string","examples":["19:30"]},"days_of_week":{"type":"array","description":"Weekdays the slot recurs on, 0 = Sunday.","items":{"type":"integer","minimum":0,"maximum":6}},"items":{"type":"array","items":{"type":"object","properties":{"recipe_id":{"type":"string","format":"uuid"},"name":{"type":"string","description":"Free-text item, for something that is not a saved recipe."},"portion":{"type":"number"},"nutrition":{"$ref":"#/components/schemas/Nutrition"}}}},"notes":{"type":"string"},"nutrition":{"$ref":"#/components/schemas/Nutrition"}}},"GroceryList":{"type":"object","required":["name"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"name":{"type":"string","examples":["Saturday shop"]},"type":{"type":"string","examples":["manual"]},"description":{"type":"string"},"archived":{"type":"boolean","default":false},"due_date":{"type":"string","format":"date"},"items":{"type":"array","items":{"$ref":"#/components/schemas/GroceryItem"}},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"GroceryItem":{"type":"object","properties":{"key":{"type":"string","description":"Stable identifier for the line within its list."},"name":{"type":"string","examples":["unsalted butter"]},"quantity":{"type":"number"},"unit":{"type":"string"},"checked":{"type":"boolean","default":false},"recipe_id":{"type":"string","format":"uuid","description":"The recipe that contributed this line, when derived."},"aisle":{"type":"string"}}},"Wishlist":{"type":"object","required":["name"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"name":{"type":"string"},"description":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/WishlistItem"}},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"WishlistItem":{"type":"object","properties":{"itemKey":{"type":"string","description":"Stable identifier for the item within its wishlist."},"kind":{"type":"string","enum":["book","recipe","source"]},"title":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"cover_url":{"type":"string","format":"uri"},"source_v2_id":{"type":"string"},"added_at":{"type":"string","format":"date-time"}}},"CookSession":{"type":"object","description":"A live cooking session. Tracks which recipes are open so the apps can\nrestore state across devices.\n","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"user_id":{"type":"string","readOnly":true},"status":{"type":"string","enum":["active","ended"]},"recipe_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"started_at":{"type":"string","format":"date-time"},"ended_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Timer":{"type":"object","description":"A server-backed timer. Because it lives on the server it still fires\nwhen the app is backgrounded or the device restarts.\n\nTimer fields are `camelCase`, unlike the rest of the API.\n","required":["dueAt"],"properties":{"timerId":{"type":"string","format":"uuid","readOnly":true},"title":{"type":"string","examples":["Rest the dough"]},"dueAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["scheduled","paused","fired","acked","cancelled"],"readOnly":true},"remainingMs":{"type":"integer","description":"Milliseconds left when paused."},"payload":{"type":"object","additionalProperties":true,"description":"Arbitrary data echoed back when the timer fires."},"deliveryPrefs":{"type":"object","description":"How to notify when the timer fires.","properties":{"push":{"type":"boolean"},"sound":{"type":"string"}}},"sessionId":{"type":"string","format":"uuid"},"recipeId":{"type":"string","format":"uuid"},"contextType":{"type":"string"},"contextId":{"type":"string"},"idempotencyKey":{"type":"string","description":"Supply a stable key to make timer creation idempotent — a retry with\nthe same key returns the original timer instead of creating a second.\n"},"ttl":{"type":"integer","description":"Seconds until the record is expired."},"createdAt":{"type":"string","format":"date-time","readOnly":true}}},"FileRecord":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"user_id":{"type":"string","readOnly":true},"url":{"type":"string","format":"uri","description":"CDN URL for the asset."},"content_type":{"type":"string","examples":["image/jpeg"]},"size":{"type":"integer","description":"Bytes."},"width":{"type":"integer"},"height":{"type":"integer"},"blur_hash":{"type":"string"},"created_at":{"type":"string","format":"date-time","readOnly":true}}},"Household":{"type":"object","description":"A group of users who share part of their libraries.","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"name":{"type":"string"},"members":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member"]},"joined_at":{"type":"string","format":"date-time"}}}},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"HouseholdInvite":{"type":"object","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"household_id":{"type":"string","format":"uuid"},"recipient_email":{"type":"string","format":"email"},"recipient_user_id":{"type":"string"},"role":{"type":"string","enum":["admin","member"],"default":"member"},"token":{"type":"string","description":"Redeem with `POST /api/households/invites/{token}/accept`."},"status":{"type":"string","enum":["pending","accepted","revoked"]},"created_at":{"type":"string","format":"date-time","readOnly":true}}},"ShareLink":{"type":"object","description":"An unlisted public link to one resource.","properties":{"id":{"type":"string","format":"uuid","readOnly":true},"token":{"type":"string","description":"The public path segment — `https://flambe.dev/share/{token}`."},"resource_type":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]},"resource_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time","readOnly":true},"revoked_at":{"type":"string","format":"date-time"}}},"SourceV2":{"type":"object","description":"A provenance record — the cookbook, website, or social account a recipe\ncame from. Shared across users rather than owned by one.\n","properties":{"id":{"type":"string"},"display_name":{"type":"string","examples":["Salt Fat Acid Heat"]},"name":{"type":"string"},"kind":{"type":"string","enum":["book","website","social","handwritten","other"]},"platform":{"type":"string","examples":["instagram"]},"authors":{"type":"array","items":{"type":"string"}},"coverUrl":{"type":"string","format":"uri"},"coverKey":{"type":"string"},"recipe_count":{"type":"integer","readOnly":true}}},"Creator":{"type":"object","description":"A cookbook author or social creator.","properties":{"id":{"type":"string"},"display_name":{"type":"string","examples":["Samin Nosrat"]},"profiles":{"type":"array","items":{"$ref":"#/components/schemas/CreatorProfile"}}}},"CreatorProfile":{"type":"object","description":"One platform presence belonging to a creator.","properties":{"profile_key":{"type":"string","description":"`{platform}:{handle}`, e.g. `instagram:saminnosrat`."},"platform":{"type":"string","examples":["instagram"]},"handle":{"type":"string"},"url":{"type":"string","format":"uri"},"avatar_url":{"type":"string","format":"uri"},"creator_id":{"type":"string"}}},"Notification":{"type":"object","required":["title"],"properties":{"id":{"type":"string","format":"uuid","readOnly":true},"title":{"type":"string"},"subtitle":{"type":"string"},"link":{"type":"string","description":"Deep link to open when tapped."},"type":{"type":"string","examples":["import_complete"]},"read_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time","readOnly":true}}},"PushToken":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Expo or APNs device token."},"platform":{"type":"string","enum":["ios","android","web"]},"device_id":{"type":"string"},"device_name":{"type":"string"},"device_type":{"type":"string"},"app_version":{"type":"string"},"app_build":{"type":"string"},"session_id":{"type":"string"}}},"User":{"type":"object","properties":{"id":{"type":"string","description":"Clerk user id."},"email":{"type":"string","format":"email"},"household_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"is_admin":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"}}},"Paginated":{"type":"object","description":"The shape of a cursor-paginated list.","properties":{"items":{"type":"array","items":{}},"next_cursor":{"type":["string","null"],"description":"Pass as `cursor` to fetch the next page. `null` on the last page."}}}}},"paths":{"/health":{"get":{"tags":["System"],"summary":"Health check","operationId":"healthCheck","description":"Liveness probe. Requires no authentication, and is what\n[status.flambe.dev](https://status.flambe.dev) polls for the Web service\ncomponent.\n","security":[],"responses":{"200":{"description":"The service is up.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","examples":["OK"]},"timestamp":{"type":"string","format":"date-time"},"uptime":{"type":"number","description":"Process uptime in seconds."}}}}}}}}},"/api/imports":{"post":{"tags":["Imports"],"summary":"Start an import","operationId":"createImport","description":"Creates an import job and enqueues it. Returns `201` immediately with a\nrecord whose `status` is `pending` — extraction happens asynchronously\non a worker, and takes anywhere from a few seconds to a couple of\nminutes depending on the source.\n\nWhich fields are required depends on `type`:\n\n| `type` | Required | Notes |\n| --- | --- | --- |\n| `web` | `url` | Must be `https`. Recipe sites, Instagram, YouTube, TikTok. |\n| `media` | `file_ids` | Upload images first with `POST /api/files`. |\n| `text` | `text` or `text_file_id` | `text` must be at least 3 characters. |\n| `image_batch` | `file_ids` | Fans out into one child import per image. |\n\nTrack the job by polling `GET /api/imports/{id}` or by subscribing to\n`GET /api/events/stream`. When `status` becomes `completed`, `recipe_id`\npoints at the created recipe.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["web","media","text","image_batch"]},"url":{"type":"string","format":"uri","description":"Required for `web`. Rejected unless the scheme is `https`."},"file_ids":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"},"description":"Required for `media` and `image_batch`."},"file_id":{"type":"string","deprecated":true,"description":"Legacy single-file alias for `file_ids`."},"text":{"type":"string","minLength":3,"description":"Raw recipe text, for `type: text`."},"text_file_id":{"type":"string","format":"uuid","description":"An uploaded text file, as an alternative to `text`."},"source_url":{"type":"string","format":"uri","description":"Attribution URL for a `text` import. Must be `https`."},"source_section":{"type":"string","maxLength":120,"description":"Chapter or page the recipe came from."},"youtube_extract_video":{"type":"boolean","description":"For YouTube `web` imports, also analyze the video frames\nrather than only the description and transcript.\n"},"batch_id":{"type":"string","format":"uuid","description":"Attach this import to an existing batch parent."},"cover_file_ids":{"type":"array","maxItems":12,"items":{"type":"string","format":"uuid"},"description":"Uploaded images to consider as the recipe cover."}}},"examples":{"web":{"summary":"Import from a URL","value":{"type":"web","url":"https://www.seriouseats.com/best-chocolate-chip-cookies"}},"cookbookPages":{"summary":"Two photographed cookbook pages","value":{"type":"media","file_ids":["6f1c2e5a-6f0a-4c4e-9d2f-2c5a5b9e1a11","7a2d3f6b-7b1b-4d5f-8e3a-3d6b6c0f2b22"],"source_section":"Chapter 4 — Breads"}},"pastedText":{"summary":"Parse pasted text","value":{"type":"text","text":"Brown Butter Cookies\n2 1/4 cups flour, 1 cup brown butter, 2 eggs...\n","source_url":"https://example.com/recipe"}}}}}},"responses":{"201":{"description":"Import created and enqueued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Import"},"examples":{"created":{"value":{"id":"4d1f8c2e-9a3b-4f1e-8c7d-1b2a3c4d5e6f","user_id":"user_2abcDEF","type":"web","status":"pending","url":"https://www.seriouseats.com/best-chocolate-chip-cookies","created_at":"2026-09-10T18:22:04.512Z","updated_at":"2026-09-10T18:22:04.512Z"}}}}}},"400":{"description":"Validation failed, or the payload does not satisfy the rules for its\n`type` — a `web` import without a `url`, a non-`https` URL, a\n`media` import with no `file_ids`, or a `text` import with neither\n`text` nor `text_file_id`.\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/ValidationError"}]},"examples":{"missingUrl":{"value":{"error":"url is required for web imports"}},"notHttps":{"value":{"error":"Only https URLs are allowed"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/TooManyRequests"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/v2":{"get":{"tags":["Imports"],"summary":"List imports (paginated)","operationId":"listImports","description":"The full import archive, newest first by `created_at`, cursor\npaginated. Use this to page back through history; use\n`/api/imports/working-set` for the bounded set a UI renders.\n","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of imports.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Paginated"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Import"}}}}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/working-set":{"get":{"tags":["Imports"],"summary":"List the import working set","operationId":"getImportWorkingSet","description":"The newest imports by `created_at`, intentionally not paginated — this\nis the bounded window the apps' import screens render.\n\nOrdering is by **creation**, not by recency of activity. An old import\nthat just failed or is being retried has a fresh `updated_at` but keeps\nits original `created_at`, so it is *not* pulled into this window. Use\n`/api/imports/v2` when you need the archive.\n\nBatch parents of any child in the window are fetched and included even\nwhen the parent itself falls outside it.\n","responses":{"200":{"description":"The working set.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Import"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}":{"get":{"tags":["Imports"],"summary":"Get an import","operationId":"getImport","description":"Fetch one import. Poll this to follow a job to completion; a one- to\ntwo-second interval is plenty, and `/api/events/stream` avoids polling\naltogether.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"responses":{"200":{"description":"The import.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Import"},"examples":{"completed":{"summary":"Finished, with the recipe it produced","value":{"id":"4d1f8c2e-9a3b-4f1e-8c7d-1b2a3c4d5e6f","type":"web","status":"completed","url":"https://www.seriouseats.com/best-chocolate-chip-cookies","recipe_id":"9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d","created_at":"2026-09-10T18:22:04.512Z","updated_at":"2026-09-10T18:22:31.004Z"}},"failed":{"summary":"Failed","value":{"id":"4d1f8c2e-9a3b-4f1e-8c7d-1b2a3c4d5e6f","type":"web","status":"failed","error":"Could not fetch the page (403)"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}/retry":{"post":{"tags":["Imports"],"summary":"Retry an import","operationId":"retryImport","description":"Re-enqueue a finished import, reusing the original source. The record is\nreset to `pending` and `processing_started_at` is re-stamped, which\ngives the attempt a full retry budget rather than inheriting the elapsed\ntime of the previous one.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"responses":{"200":{"description":"Re-enqueued; the updated record is returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Import"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The import is still in flight and so cannot be retried.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}/cancel":{"post":{"tags":["Imports"],"summary":"Cancel an import","operationId":"cancelImport","description":"Cancel an import that has not finished. A worker already mid-extraction\nobserves the cancellation at its next checkpoint, so the job may run a\nlittle longer before it stops.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"responses":{"200":{"description":"Cancelled; the updated record is returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Import"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Already in a terminal state and so cannot be cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}/children":{"get":{"tags":["Imports"],"summary":"List child imports","operationId":"listImportChildren","description":"For an `image_batch` parent, the per-image child imports it fanned out\ninto. Each child succeeds or fails independently, so a batch can be\npartially complete.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"responses":{"200":{"description":"The children of this import.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Import"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}/report":{"post":{"tags":["Imports"],"summary":"Report a bad import","operationId":"reportImport","description":"Flag a completed import whose extraction was wrong. Reports feed triage\nof the extraction pipeline. One report per import per user.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200,"description":"Short category. Defaults to `bad_import`.","examples":["missing_ingredients"]},"details":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Report recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"report_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The import has not completed yet, or this user already reported it.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"notCompleted":{"value":{"error":"Import must be completed before reporting"}},"duplicate":{"value":{"error":"Import has already been reported"}}}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/imports/{id}/source":{"post":{"tags":["Imports","Sources"],"summary":"Attach provenance to an import","operationId":"setImportSource","description":"Tell the pipeline where this import came from — a cookbook, a website, a\ncreator — before or while it runs, so the resulting recipe is attributed\ncorrectly.\n","parameters":[{"$ref":"#/components/parameters/ImportId"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"object","additionalProperties":true,"description":"A single provenance descriptor."},"sources":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Several descriptors, for a batch."},"mode":{"type":"string","description":"How to combine with provenance already attached."}}}}}},"responses":{"200":{"description":"Provenance attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Import"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes":{"get":{"tags":["Recipes"],"summary":"List recipes","operationId":"listRecipes","description":"The authenticated user's recipe library, plus any recipes shared with\nthem through a household.\n\nSupports two independent mechanisms:\n\n- **Pagination** — `limit` (default 24, max 100) and `cursor`.\n- **Delta sync** — pass `updated_since` to receive only recipes changed\n  since that instant, along with `revoked_ids` for recipes that are no\n  longer visible. Mirror `server_timestamp` back as the next\n  `updated_since`.\n\nChange history is retained for 30 days. A watermark older than 25 days\nis refused with `full_sync_required: true` and an empty result — drop\nyour local state and re-sync without `updated_since`.\n","parameters":[{"name":"updated_since","in":"query","required":false,"description":"Delta-sync watermark, ISO 8601. Omit for a full list.","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":24}},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Recipes, and the watermark to use next time.","content":{"application/json":{"schema":{"type":"object","properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"revoked_ids":{"type":"array","description":"Recipes no longer visible — deleted, or un-shared. Only\nmeaningful during a delta sync.\n","items":{"type":"string","format":"uuid"}},"server_timestamp":{"type":"string","format":"date-time","description":"Use as the next `updated_since`."},"full_sync_required":{"type":"boolean","description":"`true` when the supplied watermark is too old to serve.\nDiscard local state and re-sync in full.\n"},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Recipes"],"summary":"Create a recipe","operationId":"createRecipe","description":"Create a recipe directly, without going through an import. Only `title`\nis required.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Recipe"},{"type":"object","required":["title"]}]},"examples":{"minimal":{"summary":"Title only","value":{"title":"Weeknight carbonara"}},"full":{"summary":"A complete recipe","value":{"title":"Brown Butter Chocolate Chip Cookies","subtitle":"Makes two dozen","tags":["dessert","cookies"],"prep_time":20,"cook_time":12,"servings":24,"difficulty":"easy","ingredients":[{"name":"all-purpose flour","quantity":2.25,"unit":"cups"},{"name":"unsalted butter","quantity":1,"unit":"cup","preparation":"browned"},{"name":"flaky salt","isOptional":true}],"instructions":["Brown the butter and let it cool to room temperature.","Cream with both sugars, then beat in the eggs.","Bake at 350°F for 12 minutes."]}}}}}},"responses":{"201":{"description":"Recipe created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/changes":{"get":{"tags":["Recipes"],"summary":"Stream recipe change events","operationId":"listRecipeChanges","description":"An append-only event log of changes to the user's recipes — the\nlower-level primitive behind delta sync, useful when you need to know\n*what* changed rather than just the current state.\n\n`since` is a numeric sequence watermark, not a timestamp. Start at `0`\nand carry forward the value the response returns.\n","parameters":[{"name":"since","in":"query","required":false,"description":"Sequence watermark. Defaults to `0` (from the beginning).","schema":{"type":"integer","minimum":0,"default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":100}},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of change events.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"recipe_id":{"type":"string","format":"uuid"},"type":{"type":"string","examples":["updated"]},"seq":{"type":"integer"},"at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]}}}}}},"400":{"description":"`since` or `limit` was not a valid non-negative number.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"badSince":{"value":{"error":"Invalid since parameter"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"summary":"Get a recipe","operationId":"getRecipe","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"responses":{"200":{"description":"The recipe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Recipes"],"summary":"Update a recipe","operationId":"updateRecipe","description":"Replaces the supplied fields. Omitted fields are left untouched, so this\nbehaves as a partial update despite being a `PUT`.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"},"examples":{"rate":{"summary":"Rate a recipe and add a note","value":{"rating":5,"notes":"Chilled the dough overnight — better."}}}}}},"responses":{"200":{"description":"The updated recipe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Recipes"],"summary":"Delete a recipe","operationId":"deleteRecipe","description":"Permanently deletes the recipe and records a revocation so that syncing\nclients remove their copies.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/nutrition":{"post":{"tags":["Recipes"],"summary":"Compute nutrition","operationId":"computeRecipeNutrition","description":"Estimates per-serving nutrition by matching each ingredient against USDA\nFoodData Central and Open Food Facts, then stores the result on the\nrecipe. Values are estimates; ingredients that cannot be matched are\nskipped rather than guessed.\n\nThe result is cached against a hash of the ingredient list, so calling\nthis again without editing ingredients returns the stored figures.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"responses":{"200":{"description":"Nutrition for the recipe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Nutrition"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/save-copy":{"post":{"tags":["Recipes"],"summary":"Save a copy","operationId":"saveRecipeCopy","description":"Copies a recipe that is visible to the caller — typically one shared\nthrough a household — into their own library as an independent record.\nLater edits to the original do not propagate.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"responses":{"201":{"description":"The new copy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/source":{"put":{"tags":["Recipes","Sources"],"summary":"Set legacy source","operationId":"setRecipeSource","deprecated":true,"description":"The v1 provenance model, kept for older clients. Use\n`PUT /api/recipes/{id}/source-v2` or `POST /api/recipes/{id}/sources`.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Source set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/source-v2":{"put":{"tags":["Recipes","Sources"],"summary":"Set the primary source","operationId":"setRecipeSourceV2","description":"Attaches the recipe to a shared `SourceV2` provenance record. Supply\neither `source` to resolve or create one by descriptor, or\n`source_v2_id` to point at an existing record.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"object","additionalProperties":true,"description":"Descriptor to resolve to an existing source, or create."},"source_v2_id":{"type":"string","description":"Point at a source that already exists."},"source_ref":{"type":"object","additionalProperties":true,"description":"Where in the source this recipe sits — page, chapter, URL."}}}}}},"responses":{"200":{"description":"Source attached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Recipes","Sources"],"summary":"Clear the primary source","operationId":"clearRecipeSourceV2","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"responses":{"200":{"description":"Source cleared.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/sources":{"post":{"tags":["Recipes","Sources"],"summary":"Attach a source","operationId":"attachRecipeSource","description":"A recipe can cite more than one source — the cookbook it came from and\nthe creator's post about it, say. This attaches an additional one.\nPass `make_primary` to promote it to the headline attribution.\n","parameters":[{"$ref":"#/components/parameters/RecipeId"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"provenance":{"type":"object","additionalProperties":true,"description":"Descriptor to resolve or create."},"source_v2_id":{"type":"string"},"ref":{"type":"object","additionalProperties":true,"description":"Location within the source."},"make_primary":{"type":"boolean","default":false}}}}}},"responses":{"201":{"description":"Attachment created.","content":{"application/json":{"schema":{"type":"object","properties":{"attachment_id":{"type":"string","format":"uuid"},"recipe":{"$ref":"#/components/schemas/Recipe"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/recipes/{id}/sources/{attachmentId}":{"patch":{"tags":["Recipes","Sources"],"summary":"Update a source attachment","operationId":"updateRecipeSourceAttachment","parameters":[{"$ref":"#/components/parameters/RecipeId"},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"ref":{"type":"object","additionalProperties":true},"make_primary":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Attachment updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Recipes","Sources"],"summary":"Detach a source","operationId":"detachRecipeSource","parameters":[{"$ref":"#/components/parameters/RecipeId"},{"name":"attachmentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Detached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections":{"get":{"tags":["Collections"],"summary":"List collections","operationId":"listCollections","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"The user's collections, including any shared with them.","content":{"application/json":{"schema":{"type":"object","properties":{"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Collections"],"summary":"Create a collection","operationId":"createCollection","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"recipes":{"type":"array","description":"Recipe ids to seed the collection with.","items":{"type":"string","format":"uuid"}}}},"examples":{"seeded":{"value":{"name":"Weeknight dinners","description":"30 minutes or less","recipes":["9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d"]}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections/{id}":{"get":{"tags":["Collections"],"summary":"Get a collection","operationId":"getCollection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The collection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Collections"],"summary":"Update a collection","operationId":"updateCollection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"recipes":{"type":"array","description":"Replaces the membership list wholesale.","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Collections"],"summary":"Delete a collection","operationId":"deleteCollection","description":"Deletes the collection. The recipes in it are not deleted.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections/{id}/recipes":{"post":{"tags":["Collections"],"summary":"Add a recipe to a collection","operationId":"addRecipeToCollection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["recipeId"],"properties":{"recipeId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections/{id}/recipes/bulk":{"post":{"tags":["Collections"],"summary":"Add several recipes to a collection","operationId":"addRecipesToCollectionBulk","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["recipeIds"],"properties":{"recipeIds":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections/{id}/recipes/{recipeId}":{"delete":{"tags":["Collections"],"summary":"Remove a recipe from a collection","operationId":"removeRecipeFromCollection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"recipeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Collection"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/collections/{id}/leave":{"post":{"tags":["Collections","Households"],"summary":"Leave a shared collection","operationId":"leaveCollection","description":"Removes the caller's access to a collection that was shared with them.\nThe owner's copy is untouched. Owners cannot leave their own collection\n— delete it instead.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Left the collection.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mealplans":{"get":{"tags":["Meal plans"],"summary":"List meal plans","operationId":"listMealPlans","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"The user's meal plans.","content":{"application/json":{"schema":{"type":"object","properties":{"mealplans":{"type":"array","items":{"$ref":"#/components/schemas/MealPlan"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Meal plans"],"summary":"Create a meal plan","operationId":"createMealPlan","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"end_date":{"type":"string","format":"date"}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mealplans/{id}":{"get":{"tags":["Meal plans"],"summary":"Get a meal plan","operationId":"getMealPlan","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The meal plan, with its meals.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Meal plans"],"summary":"Update a meal plan","operationId":"updateMealPlan","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"end_date":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Meal plans"],"summary":"Delete a meal plan","operationId":"deleteMealPlan","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mealplans/{id}/meals":{"post":{"tags":["Meal plans"],"summary":"Add a meal to a plan","operationId":"addMeal","description":"Adds a recurring slot. `days_of_week` makes the slot repeat — `[1,3,5]`\nis Monday, Wednesday and Friday.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Meal"},{"type":"object","required":["start_time","end_time","days_of_week","items"]}]},"examples":{"dinner":{"value":{"name":"Dinner","start_time":"18:00","end_time":"19:30","days_of_week":[1,3,5],"items":[{"recipe_id":"9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d","portion":1},{"name":"green salad"}]}}}}}},"responses":{"201":{"description":"Meal added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mealplans/{id}/meals/{mealId}":{"put":{"tags":["Meal plans"],"summary":"Update a meal","operationId":"updateMeal","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"mealId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Meal"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Meal plans"],"summary":"Remove a meal","operationId":"deleteMeal","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"mealId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MealPlan"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/mealplans/{id}/export/{format}":{"get":{"tags":["Meal plans"],"summary":"Export a meal plan","operationId":"exportMealPlan","description":"Renders the plan for use outside Flambe.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"path","required":true,"description":"Output format.","schema":{"type":"string","examples":["ics"]}}],"responses":{"200":{"description":"The exported plan, in the requested format.","content":{"text/calendar":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unsupported format.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/grocery-lists":{"get":{"tags":["Grocery lists"],"summary":"List grocery lists","operationId":"listGroceryLists","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"The user's grocery lists.","content":{"application/json":{"schema":{"type":"object","properties":{"grocery_lists":{"type":"array","items":{"$ref":"#/components/schemas/GroceryList"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Grocery lists"],"summary":"Create a grocery list","operationId":"createGroceryList","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/GroceryList"},{"type":"object","required":["name"]}]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/grocery-lists/{id}":{"get":{"tags":["Grocery lists"],"summary":"Get a grocery list","operationId":"getGroceryList","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The list, with its items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Grocery lists"],"summary":"Update a grocery list","operationId":"updateGroceryList","description":"Supplying `items` replaces the whole item list — this is how a client\npersists check-offs and edits in one call.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryList"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Grocery lists"],"summary":"Delete a grocery list","operationId":"deleteGroceryList","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/grocery-lists/{id}/items":{"post":{"tags":["Grocery lists"],"summary":"Append items to a grocery list","operationId":"addGroceryItems","description":"Adds items without rewriting the list. Lines that match an existing item\nare merged rather than duplicated.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GroceryItem"}}}},"examples":{"fromRecipe":{"value":{"items":[{"name":"unsalted butter","quantity":1,"unit":"cup"},{"name":"flaky salt"}]}}}}}},"responses":{"200":{"description":"Items appended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroceryList"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/grocery-lists/{id}/leave":{"post":{"tags":["Grocery lists","Households"],"summary":"Leave a shared grocery list","operationId":"leaveGroceryList","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Left the list.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/wishlists":{"get":{"tags":["Wishlists"],"summary":"List wishlists","operationId":"listWishlists","responses":{"200":{"description":"The user's wishlists.","content":{"application/json":{"schema":{"type":"object","properties":{"wishlists":{"type":"array","items":{"$ref":"#/components/schemas/Wishlist"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Wishlists"],"summary":"Create a wishlist","operationId":"createWishlist","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/wishlists/{id}":{"get":{"tags":["Wishlists"],"summary":"Get a wishlist","operationId":"getWishlist","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The wishlist, with its items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Wishlists"],"summary":"Update a wishlist","operationId":"updateWishlist","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Wishlists"],"summary":"Delete a wishlist","operationId":"deleteWishlist","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/wishlists/{id}/items":{"post":{"tags":["Wishlists"],"summary":"Add an item to a wishlist","operationId":"addWishlistItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WishlistItem"}}}},"responses":{"200":{"description":"Item added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Wishlists"],"summary":"Remove an item from a wishlist","operationId":"removeWishlistItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["itemKey"],"properties":{"itemKey":{"type":"string"}}}}}},"responses":{"200":{"description":"Item removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/wishlists/default/items":{"post":{"tags":["Wishlists"],"summary":"Add an item to the default wishlist","operationId":"addDefaultWishlistItem","description":"Adds to the user's default wishlist, creating it if they have none. Use\nthis for a one-tap \"save for later\" that does not make the caller pick a\nlist first.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WishlistItem"}}}},"responses":{"200":{"description":"Item added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Wishlist"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/files":{"post":{"tags":["Files"],"summary":"Upload files","operationId":"uploadFiles","description":"Uploads one or more binary assets as `multipart/form-data` under the\nfield name `files`. Images are stored, processed for width variants and\ngiven a BlurHash.\n\nThis is the first half of a `media` import: upload the images here, then\npass the returned ids as `file_ids` to `POST /api/imports`.\n","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"files":{"type":"array","description":"One or more files, all under the field name `files`.","items":{"type":"string","format":"binary"}}}},"encoding":{"files":{"contentType":"image/jpeg, image/png, image/heic, image/webp"}}}}},"responses":{"201":{"description":"Stored. Ids are in creation order.","content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"$ref":"#/components/schemas/FileRecord"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"description":"Upload failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"failed":{"value":{"error":"Upload failed"}}}}}}}}},"/api/files/{id}":{"get":{"tags":["Files"],"summary":"Get a file","operationId":"getFile","description":"Returns the asset for a file the caller can read — their own, or one\nshared through a household. Prefer the CDN `url` from the file's\nmetadata for anything user-facing; it is cached at the edge.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The file.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/files/{id}/metadata":{"get":{"tags":["Files"],"summary":"Get file metadata","operationId":"getFileMetadata","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Metadata for the file.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileRecord"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/files/metadata":{"get":{"tags":["Files"],"summary":"List file metadata","operationId":"listFileMetadata","description":"Every file readable by the caller, paginated.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of file metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FileRecord"}},"next_cursor":{"type":["string","null"]}}}}}},"400":{"description":"The cursor could not be decoded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"badCursor":{"value":{"error":"Invalid cursor"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/files/metadata/query":{"post":{"tags":["Files"],"summary":"Resolve file metadata in bulk","operationId":"queryFileMetadata","description":"Resolves many file ids in one call. Ids the caller cannot read are\nomitted from the response rather than erroring, so a partial result is\nnormal.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"}}}},"examples":{"batch":{"value":{"ids":["6f1c2e5a-6f0a-4c4e-9d2f-2c5a5b9e1a11","7a2d3f6b-7b1b-4d5f-8e3a-3d6b6c0f2b22"]}}}}}},"responses":{"200":{"description":"Metadata for the readable subset of the requested ids.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/FileRecord"}}}}}}},"400":{"description":"`ids` was missing or empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"empty":{"value":{"error":"ids must be a non-empty array of strings"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/files/metadata/stream":{"post":{"tags":["Files"],"summary":"Stream file metadata","operationId":"streamFileMetadata","description":"The same resolution as `/metadata/query`, delivered as newline-delimited\nJSON (`application/x-ndjson`) so a client can render the first records\nwithout waiting for the whole batch. One JSON object per line.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","minItems":1,"items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"One JSON object per line, in no guaranteed order.","content":{"application/x-ndjson":{"schema":{"type":"string"}}}},"400":{"description":"`ids` was missing or empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions":{"get":{"tags":["Cook sessions"],"summary":"List cook sessions","operationId":"listCookSessions","responses":{"200":{"description":"The user's sessions, most recent first.","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"$ref":"#/components/schemas/CookSession"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Cook sessions"],"summary":"Start a cook session","operationId":"createCookSession","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"recipe_ids":{"type":"array","description":"Recipes to open in the session.","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"201":{"description":"Session started.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/active":{"get":{"tags":["Cook sessions"],"summary":"Get the active session","operationId":"getActiveCookSession","description":"The session currently in progress, so a second device can join what is\nalready being cooked. Returns `null` when nothing is active.\n","responses":{"200":{"description":"The active session, or `null`.","content":{"application/json":{"schema":{"type":"object","properties":{"session":{"oneOf":[{"$ref":"#/components/schemas/CookSession"},{"type":"null"}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/{id}":{"get":{"tags":["Cook sessions"],"summary":"Get a cook session","operationId":"getCookSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Cook sessions"],"summary":"Delete a cook session","operationId":"deleteCookSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/{id}/end":{"post":{"tags":["Cook sessions"],"summary":"End a cook session","operationId":"endCookSession","description":"Marks the session `ended` and stamps `ended_at`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Ended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/{id}/restore":{"post":{"tags":["Cook sessions"],"summary":"Restore an ended session","operationId":"restoreCookSession","description":"Reopens a session that was ended — for when cooking was interrupted\nrather than finished.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Restored to `active`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The session is already active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/{id}/recipes":{"post":{"tags":["Cook sessions"],"summary":"Add a recipe to a session","operationId":"addRecipeToCookSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["recipe_id"],"properties":{"recipe_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sessions/{id}/recipes/{recipeId}":{"delete":{"tags":["Cook sessions"],"summary":"Remove a recipe from a session","operationId":"removeRecipeFromCookSession","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"recipeId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CookSession"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers":{"get":{"tags":["Timers"],"summary":"List timers","operationId":"listTimers","responses":{"200":{"description":"The user's timers.","content":{"application/json":{"schema":{"type":"object","properties":{"timers":{"type":"array","items":{"$ref":"#/components/schemas/Timer"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Timers"],"summary":"Create a timer","operationId":"createTimer","description":"Schedules a timer to fire at `dueAt`. Because the schedule lives on the\nserver, it still fires if the app is backgrounded or the device\nrestarts.\n\nPass `idempotencyKey` to make retries safe — a second call with the same\nkey returns the original timer rather than creating a duplicate.\n\nNote that timer fields are `camelCase`, unlike the rest of the API.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Timer"},{"type":"object","required":["dueAt"]}]},"examples":{"rest":{"value":{"title":"Rest the dough","dueAt":"2026-09-10T19:00:00.000Z","recipeId":"9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d","idempotencyKey":"rest-dough-9b8a7c6d"}}}}}},"responses":{"200":{"description":"An existing timer matched `idempotencyKey` and is returned unchanged.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"201":{"description":"Timer scheduled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers/{timerId}":{"get":{"tags":["Timers"],"summary":"Get a timer","operationId":"getTimer","parameters":[{"name":"timerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The timer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers/{timerId}/ack":{"post":{"tags":["Timers"],"summary":"Acknowledge a fired timer","operationId":"ackTimer","description":"Confirms the user saw the alert, which stops further reminders for it.\n","parameters":[{"name":"timerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Acknowledged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers/{timerId}/pause":{"patch":{"tags":["Timers"],"summary":"Pause a timer","operationId":"pauseTimer","description":"Freezes the countdown and records `remainingMs`.","parameters":[{"name":"timerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The timer is not in a pausable state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers/{timerId}/resume":{"patch":{"tags":["Timers"],"summary":"Resume a paused timer","operationId":"resumeTimer","description":"Restarts the countdown from `remainingMs`. Pass `dueAt` to resume to an\nexplicit instant instead.\n","parameters":[{"name":"timerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"dueAt":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Resumed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The timer is not paused.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/timers/{timerId}/cancel":{"patch":{"tags":["Timers"],"summary":"Cancel a timer","operationId":"cancelTimer","parameters":[{"name":"timerId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households":{"post":{"tags":["Households"],"summary":"Create a household","operationId":"createHousehold","description":"The caller becomes the owner.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Household"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/me":{"get":{"tags":["Households"],"summary":"List my households","operationId":"listMyHouseholds","responses":{"200":{"description":"Households the caller belongs to.","content":{"application/json":{"schema":{"type":"object","properties":{"households":{"type":"array","items":{"$ref":"#/components/schemas/Household"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}":{"get":{"tags":["Households"],"summary":"Get a household","operationId":"getHousehold","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The household, with its members.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Household"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Households"],"summary":"Rename a household","operationId":"updateHousehold","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Household"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"delete":{"tags":["Households"],"summary":"Delete a household","operationId":"deleteHousehold","description":"Owner only. Dissolves the household and revokes every share made\nthrough it; members keep their own libraries.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/leave":{"post":{"tags":["Households"],"summary":"Leave a household","operationId":"leaveHousehold","description":"Removes the caller and revokes the resources they had shared into it.\nAn owner must transfer ownership or delete the household instead.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Left.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/invites":{"post":{"tags":["Households"],"summary":"Invite someone to a household","operationId":"createHouseholdInvite","description":"Invite by `recipient_email` for someone who may not have an account\nyet, or by `recipient_user_id` for an existing Flambe user.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"recipient_email":{"type":"string","format":"email"},"recipient_user_id":{"type":"string"},"role":{"type":"string","enum":["admin","member"],"default":"member"}}}}}},"responses":{"201":{"description":"Invite created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HouseholdInvite"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/invites/{inviteId}":{"delete":{"tags":["Households"],"summary":"Revoke an invite","operationId":"revokeHouseholdInvite","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"inviteId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/invites/{token}/accept":{"post":{"tags":["Households"],"summary":"Accept an invite","operationId":"acceptHouseholdInvite","description":"Redeems an invite token and joins the caller to the household. Requires\nauthentication — the invite is bound to whoever accepts it.\n","parameters":[{"name":"token","in":"path","required":true,"description":"The invite's `token`.","schema":{"type":"string"}}],"responses":{"200":{"description":"Joined.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Household"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such invite, or it was revoked or already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The caller already belongs to this household.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/members/{userId}":{"delete":{"tags":["Households"],"summary":"Remove a member","operationId":"removeHouseholdMember","description":"Owner or admin only.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/library":{"get":{"tags":["Households"],"summary":"List the shared library","operationId":"getHouseholdLibrary","description":"Every resource shared into the household, by any member.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The shared library.","content":{"application/json":{"schema":{"type":"object","properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"collections":{"type":"array","items":{"$ref":"#/components/schemas/Collection"}},"mealplans":{"type":"array","items":{"$ref":"#/components/schemas/MealPlan"}},"grocerylists":{"type":"array","items":{"$ref":"#/components/schemas/GroceryList"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/library/{resourceType}":{"post":{"tags":["Households"],"summary":"Share a resource into a household","operationId":"grantHouseholdResource","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"resourceType","in":"path","required":true,"schema":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resource_id"],"properties":{"resource_id":{"type":"string","format":"uuid"},"role":{"type":"string","description":"Access level granted to the other members.","enum":["viewer","editor"],"default":"viewer"}}}}}},"responses":{"201":{"description":"Shared.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/library/{resourceType}/bulk":{"put":{"tags":["Households"],"summary":"Replace the grants for a resource type","operationId":"bulkGrantHouseholdResources","description":"Sets the complete set of grants for one resource type in a single call.\nAnything the caller had shared that is absent from `grants` is revoked,\nwhich is what makes this safe to drive from a checkbox list.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"resourceType","in":"path","required":true,"schema":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["grants"],"properties":{"grants":{"type":"array","items":{"type":"object","required":["resource_id"],"properties":{"resource_id":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["viewer","editor"],"default":"viewer"}}}}}}}}},"responses":{"200":{"description":"Grants replaced.","content":{"application/json":{"schema":{"type":"object","properties":{"granted":{"type":"integer"},"revoked":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/library/{resourceType}/{resourceId}":{"delete":{"tags":["Households"],"summary":"Unshare a resource","operationId":"revokeHouseholdResource","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"resourceType","in":"path","required":true,"schema":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]}},{"name":"resourceId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Unshared.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/library/my-resources/{resourceType}":{"get":{"tags":["Households"],"summary":"List my shareable resources","operationId":"listMyHouseholdResources","description":"The caller's own resources of one type, each annotated with whether it\nis currently shared into this household — the data behind a share\npicker.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"resourceType","in":"path","required":true,"schema":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]}}],"responses":{"200":{"description":"The caller's resources and their share state.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"resource_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"shared":{"type":"boolean"},"role":{"type":"string","enum":["viewer","editor"]}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/households/{id}/sharing/preferences":{"get":{"tags":["Households"],"summary":"Get sharing preferences","operationId":"getHouseholdSharingPreferences","description":"The caller's defaults for this household — for example, whether newly\ncreated recipes are shared automatically.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The preferences.","content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Households"],"summary":"Set sharing preferences","operationId":"setHouseholdSharingPreferences","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["preferences"],"properties":{"preferences":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","properties":{"preferences":{"type":"object","additionalProperties":true}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/share-links":{"post":{"tags":["Sharing"],"summary":"Create a share link","operationId":"createShareLink","description":"Mints an unlisted public URL for one resource. Anyone with the link can\nview it without signing in, so treat the token as a secret. Share links\nare deliberately excluded from `sitemap.xml` and disallowed in\n`robots.txt`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resource_type","resource_id"],"properties":{"resource_type":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]},"resource_id":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"Link created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareLink"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/share-links/{id}":{"delete":{"tags":["Sharing"],"summary":"Revoke a share link","operationId":"revokeShareLink","description":"Revokes the link immediately. The URL stops resolving; any page already\nopen keeps whatever it has already loaded.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/shared/{token}":{"get":{"tags":["Sharing"],"summary":"Resolve a share link","operationId":"getSharedResource","description":"Resolves a share token to the resource it points at. **Unauthenticated**\n— this is what renders `https://flambe.dev/share/{token}` for a\nrecipient who has no Flambe account.\n","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The shared resource.","content":{"application/json":{"schema":{"type":"object","properties":{"resource_type":{"type":"string","enum":["recipe","collection","mealplan","grocerylist"]},"resource":{"oneOf":[{"$ref":"#/components/schemas/Recipe"},{"$ref":"#/components/schemas/Collection"},{"$ref":"#/components/schemas/MealPlan"},{"$ref":"#/components/schemas/GroceryList"}]}}}}}},"404":{"description":"Unknown or revoked token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/shared/{token}/save":{"post":{"tags":["Sharing"],"summary":"Save a shared resource","operationId":"saveSharedResource","description":"Copies a shared resource into the caller's own library. Unlike\nresolving the link, this does require authentication.\n","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"Saved; the new copy is returned.","content":{"application/json":{"schema":{"type":"object","properties":{"resource_type":{"type":"string"},"resource":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Unknown or revoked token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sources":{"get":{"tags":["Sources"],"summary":"List sources (v1)","operationId":"listSources","deprecated":true,"description":"The v1 provenance model. Prefer `/api/v2/sources`.","responses":{"200":{"description":"Sources referenced by the caller's recipes.","content":{"application/json":{"schema":{"type":"object","properties":{"sources":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/sources/{id}/recipes":{"get":{"tags":["Sources"],"summary":"List a source's recipes (v1)","operationId":"listSourceRecipes","deprecated":true,"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Recipes attributed to the source.","content":{"application/json":{"schema":{"type":"object","properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/sources":{"get":{"tags":["Sources"],"summary":"List sources","operationId":"listSourcesV2","description":"Sources referenced by recipes the caller can see.","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Sources.","content":{"application/json":{"schema":{"type":"object","properties":{"sources":{"type":"array","items":{"$ref":"#/components/schemas/SourceV2"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/sources/{id}":{"get":{"tags":["Sources"],"summary":"Get a source","operationId":"getSourceV2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The source.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceV2"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"put":{"tags":["Sources"],"summary":"Update a source","operationId":"updateSourceV2","description":"Sources are shared across users, so corrections here — a fixed title, a\nbetter cover — are visible to everyone citing the same source.\n","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"display_name":{"type":"string"},"name":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["book","website","social","handwritten","other"]},"platform":{"type":"string"},"coverUrl":{"type":"string","format":"uri"},"coverKey":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceV2"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/sources/{id}/recipes":{"get":{"tags":["Sources"],"summary":"List a source's recipes","operationId":"listSourceV2Recipes","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Recipes attributed to the source.","content":{"application/json":{"schema":{"type":"object","properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/creators":{"get":{"tags":["Creators"],"summary":"List creators","operationId":"listCreators","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Creators.","content":{"application/json":{"schema":{"type":"object","properties":{"creators":{"type":"array","items":{"$ref":"#/components/schemas/Creator"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/creators/{id}":{"get":{"tags":["Creators"],"summary":"Get a creator","operationId":"getCreator","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The creator.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Creator"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}},"patch":{"tags":["Creators"],"summary":"Rename a creator","operationId":"updateCreator","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["display_name"],"properties":{"display_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Creator"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/creators/{id}/profiles":{"get":{"tags":["Creators"],"summary":"List a creator's profiles","operationId":"listCreatorProfiles","description":"The platform accounts belonging to one creator.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Profiles.","content":{"application/json":{"schema":{"type":"object","properties":{"profiles":{"type":"array","items":{"$ref":"#/components/schemas/CreatorProfile"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/creator-profiles":{"get":{"tags":["Creators"],"summary":"List creator profiles","operationId":"listAllCreatorProfiles","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Profiles.","content":{"application/json":{"schema":{"type":"object","properties":{"profiles":{"type":"array","items":{"$ref":"#/components/schemas/CreatorProfile"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/creator-profiles/{profile_key}":{"get":{"tags":["Creators"],"summary":"Get a creator profile","operationId":"getCreatorProfile","parameters":[{"name":"profile_key","in":"path","required":true,"description":"`{platform}:{handle}`, URL-encoded.","schema":{"type":"string","examples":["instagram:saminnosrat"]}}],"responses":{"200":{"description":"The profile.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatorProfile"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/verified-recipes":{"get":{"tags":["Sources"],"summary":"List verified recipes","operationId":"listVerifiedRecipes","description":"Recipes whose extraction has been checked against the original source.\nUseful as a trusted corpus.\n","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Verified recipes.","content":{"application/json":{"schema":{"type":"object","properties":{"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/v2/verified-recipes/{id}":{"get":{"tags":["Sources"],"summary":"Get a verified recipe","operationId":"getVerifiedRecipe","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The verified recipe.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Recipe"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/notifications":{"get":{"tags":["Notifications"],"summary":"List notifications","operationId":"listNotifications","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Notifications, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"notifications":{"type":"array","items":{"$ref":"#/components/schemas/Notification"}},"next_cursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}},"post":{"tags":["Notifications"],"summary":"Create a notification","operationId":"createNotification","description":"Writes a notification to the caller's own feed. Most notifications are\nproduced by the platform — import completions and household invites —\nso this is mainly for a client that wants to surface something locally.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title"],"properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"link":{"type":"string"},"type":{"type":"string"}}}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/notifications/unread-count":{"get":{"tags":["Notifications"],"summary":"Get the unread count","operationId":"getUnreadNotificationCount","description":"Cheap enough to poll for a badge.","responses":{"200":{"description":"The count.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/notifications/{id}/read":{"patch":{"tags":["Notifications"],"summary":"Mark a notification read","operationId":"markNotificationRead","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/notifications/{id}/unread":{"patch":{"tags":["Notifications"],"summary":"Mark a notification unread","operationId":"markNotificationUnread","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Marked unread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Notification"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/notifications/mark-all-read":{"post":{"tags":["Notifications"],"summary":"Mark all notifications read","operationId":"markAllNotificationsRead","description":"Pass `before` to only clear notifications older than that instant, which\navoids swallowing something that arrived while the user was reading.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"before":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Cleared.","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/push-tokens":{"post":{"tags":["Push tokens"],"summary":"Register a device for push","operationId":"registerPushToken","description":"Registering the same `token` again updates the existing record rather\nthan creating a duplicate, so this is safe to call on every launch.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PushToken"},{"type":"object","required":["token"]}]}}}},"responses":{"201":{"description":"Registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushToken"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/push-tokens/me":{"get":{"tags":["Push tokens"],"summary":"List my registered devices","operationId":"listMyPushTokens","responses":{"200":{"description":"The caller's registered devices.","content":{"application/json":{"schema":{"type":"object","properties":{"tokens":{"type":"array","items":{"$ref":"#/components/schemas/PushToken"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/push-tokens/{token}":{"delete":{"tags":["Push tokens"],"summary":"Unregister a device","operationId":"deletePushToken","description":"Call this on sign-out so the device stops receiving pushes.","parameters":[{"name":"token","in":"path","required":true,"description":"The device token, URL-encoded.","schema":{"type":"string"}}],"responses":{"200":{"description":"Unregistered.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/books/search":{"get":{"tags":["Books"],"summary":"Search cookbooks","operationId":"searchBooks","description":"Looks up cookbooks by title, for attributing a recipe to a book. Backed\nby an external bibliographic catalogue.\n","parameters":[{"name":"q","in":"query","required":true,"description":"Search text.","schema":{"type":"string","examples":["salt fat acid heat"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20,"default":8}}],"responses":{"200":{"description":"Matching books.","content":{"application/json":{"schema":{"type":"object","properties":{"books":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"authors":{"type":"array","items":{"type":"string"}},"published_year":{"type":"integer"},"cover_url":{"type":"string","format":"uri"},"isbn":{"type":"string"}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/books/by-author":{"get":{"tags":["Books"],"summary":"List cookbooks by author","operationId":"listBooksByAuthor","parameters":[{"name":"name","in":"query","required":true,"description":"Author name.","schema":{"type":"string","examples":["Samin Nosrat"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":40,"default":20}}],"responses":{"200":{"description":"Books by that author.","content":{"application/json":{"schema":{"type":"object","properties":{"books":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/ai/embed-query":{"post":{"tags":["AI"],"summary":"Embed a search query","operationId":"embedQuery","description":"Returns the embedding vector for a search string, using the same model\nthat indexed the recipes. Use it to run your own semantic search over\nrecipes you have synced locally — embedding the query client-side with a\ndifferent model would not produce comparable distances.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["query"],"properties":{"query":{"type":"string","examples":["something warm with lentils"]}}}}}},"responses":{"200":{"description":"The embedding.","content":{"application/json":{"schema":{"type":"object","properties":{"embedding":{"type":"array","items":{"type":"number"}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/ai/session":{"post":{"tags":["AI"],"summary":"Create a realtime assistant session","operationId":"createAiSession","description":"Mints a short-lived client secret for the hands-free cooking assistant,\nso a client can open a realtime connection without ever holding a\nlong-lived provider key. Scope it to a recipe with `recipeId`.\n\nThe secret expires quickly — request one per session, at the moment the\nuser starts talking, rather than caching it.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"recipeId":{"type":"string","format":"uuid","description":"Ground the assistant in this recipe."},"expertise":{"type":"string","description":"How much cooking knowledge to assume."}}}}}},"responses":{"200":{"description":"An ephemeral client secret.","content":{"application/json":{"schema":{"type":"object","properties":{"clientSecret":{"type":"string"},"expiresAt":{"type":"integer","description":"Unix seconds at which the secret stops working."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"},"502":{"description":"The upstream assistant provider refused to create a session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"upstream":{"value":{"error":"Failed to create AI session"}}}}}}}}},"/api/users/me":{"get":{"tags":["Users"],"summary":"Get the current user","operationId":"getCurrentUser","description":"Resolves the bearer token to a profile. The cheapest way to check that a\ntoken is valid.\n","responses":{"200":{"description":"The authenticated user.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/events/stream":{"get":{"tags":["Events"],"summary":"Subscribe to live events","operationId":"streamEvents","description":"A [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events)\nstream of changes to the caller's data — most usefully, import progress\nand completion, which removes the need to poll `GET /api/imports/{id}`.\n\nThe connection is authenticated with the usual `Authorization: Bearer`\nheader. Note that the browser's built-in `EventSource` cannot set\nheaders, so on the web you need a fetch-based SSE client.\n\nThe server sends a comment line every few seconds as a keep-alive;\nignore lines beginning with `:`.\n","responses":{"200":{"description":"An open event stream.","content":{"text/event-stream":{"schema":{"type":"string"},"examples":{"importCompleted":{"summary":"An import finishing","value":": connected\n\nevent: import.updated\ndata: {\"id\":\"4d1f8c2e\",\"status\":\"processing\"}\n\nevent: import.completed\ndata: {\"id\":\"4d1f8c2e\",\"recipe_id\":\"9b8a7c6d\"}\n"}}}}},"401":{"description":"Missing or invalid bearer token. The response has an empty body.\n"},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/auth/register":{"post":{"tags":["Auth"],"summary":"Register (legacy)","operationId":"register","deprecated":true,"description":"Legacy username/password registration, retained for older clients.\nAuthentication is handled by Clerk now — see the\n[Authentication guide](https://docs.flambe.dev/docs/guides/authentication).\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username","email","password"],"properties":{"username":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"}}}}}},"responses":{"201":{"description":"Registered.","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"description":"The username or email is already taken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}},"/api/auth/login":{"post":{"tags":["Auth"],"summary":"Log in (legacy)","operationId":"login","deprecated":true,"description":"Legacy username/password login, retained for older clients. Use Clerk\ninstead.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string","format":"password"}}}}}},"responses":{"200":{"description":"Logged in.","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"description":"Bad credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"$ref":"#/components/responses/ServerError"}}}}}}