{
  "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"
          }
        }
      }
    }
  }
}
