{
  "openapi": "3.1.0",
  "info": {
    "title": "Merran API",
    "version": "0.0.0",
    "description": "Everything the Merran app does, it does through this API: the same routes are open to scripts and to tools like Home Assistant. Tools use an API key: make one at `POST /api/auth/api-keys` while signed in, then send it as `Authorization: Bearer mrn_…`. Each route a key can use says which permission it needs, in `x-merran-permission` (like `recipes:read`); routes without it refuse keys. Browsers sign in with `POST /api/auth/sign-in/username` and send the session cookie it sets.",
    "license": {
      "name": "AGPL-3.0-or-later",
      "url": "https://www.gnu.org/licenses/agpl-3.0.html"
    }
  },
  "servers": [
    {
      "url": "{server}",
      "description": "Your Merran",
      "variables": {
        "server": {
          "default": "http://localhost:3000",
          "description": "The address you reach Merran at"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Sign-in",
      "description": "Setting up the server, signing in and out, invite links and switching households. Signed-in requests carry the session cookie."
    },
    {
      "name": "API keys",
      "description": "Keys for scripts and tools like Home Assistant. Each acts as its person in one household, with only the permissions it was given, and stops working if it expires, is revoked, or its person leaves the household."
    },
    {
      "name": "Households",
      "description": "The household in use: its settings, its people and their roles, and invite links."
    },
    {
      "name": "Recipes",
      "description": "The household's recipes, their tags and versions, and Recently deleted. Ingredients and steps are written as lines, as in the editor, and each ingredient line is read into its amount, unit, food and note."
    },
    {
      "name": "Review",
      "description": "What AI suggests, waiting to be applied or dismissed: tidy ups of recipes, for now. AI never changes a recipe by itself. Each section of a suggestion is applied or skipped on its own, and applying keeps a version."
    },
    {
      "name": "Assistant",
      "description": "Chatting with the assistant, which knows the household's recipes, plan, list and pantry, and proposes changes rather than making them. Each person's chats are their own."
    },
    {
      "name": "Discover",
      "description": "Finding new recipes online for a theme, with AI and web search: pages Merran has opened and can import."
    },
    {
      "name": "Collections",
      "description": "Starter collections: sets of recipes to start a library with, each filled by its rule from the recipe sites Merran reads, around a few hand-picked classics."
    },
    {
      "name": "Sites",
      "description": "Recipe sites a household follows, and their newest recipes, read in the background as Merran, following robots.txt."
    },
    {
      "name": "Plan",
      "description": "The week plan: meals on days and in the week's pool, what's next, and Fill the week's ideas, each saying why. Today is always the household's, in its time zone."
    },
    {
      "name": "List",
      "description": "The shopping list, by aisle: from the plan (added up, and following the plan when meals change), typed in, or every week. Changes say what things were before, for Undo."
    },
    {
      "name": "Pantry",
      "description": "What's at home, in the fridge, the freezer and the cupboard: what to use soon, and what's set aside for planned meals. Shopping put away goes in, and cooking a planned meal uses it up."
    },
    {
      "name": "Live",
      "description": "What changes in the household, as it happens: the live updates the app uses, and who has the list open."
    },
    {
      "name": "Stores",
      "description": "Stores with an API: Kroger (Fry's, Ralphs...), its products and prices, and sending the list to a linked account's cart."
    },
    {
      "name": "Voice",
      "description": "The server voice: cook steps read aloud by Kokoro or OpenAI, the same on every device."
    },
    {
      "name": "Settings",
      "description": "Settings for the server (its admin only) and the household in use. A setting from an environment variable is locked, and a secret is never sent back: only whether it's set, and its last four characters."
    },
    {
      "name": "Server",
      "description": "Whether the server is up and ready, and which of Merran's optional parts are on. No sign-in needed."
    }
  ],
  "components": {
    "securitySchemes": {
      "session": {
        "type": "apiKey",
        "in": "cookie",
        "name": "merran.session_token",
        "description": "The cookie that signing in sets."
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mrn_…",
        "description": "An API key, made at POST /api/auth/api-keys, sent as `Authorization: Bearer mrn_…`. It acts as its person in one household, with only the permissions it was given."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "not_found"
              },
              "message": {
                "type": "string",
                "example": "There's nothing here."
              },
              "fields": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "example": {
                  "name": "Give it a name."
                }
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "ChatList": {
        "type": "object",
        "properties": {
          "chats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Chat"
            }
          }
        },
        "required": [
          "chats"
        ],
        "example": {
          "chats": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000002b",
              "title": "What can I make with leftover chicken?",
              "createdAt": "2026-10-01T17:30:00.000Z",
              "updatedAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "Chat": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string",
            "description": "From its first message."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "title",
          "createdAt",
          "updatedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002b",
          "title": "What can I make with leftover chicken?",
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ChatWithMessages": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Chat"
          },
          {
            "type": "object",
            "properties": {
              "messages": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": {}
                },
                "description": "Its messages in order, as AI SDK's UI messages: each with an id, a role, and parts (text, and the assistant's tool calls with their results)."
              }
            },
            "required": [
              "messages"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002b",
          "title": "What can I make with leftover chicken?",
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-15T18:30:00.000Z",
          "messages": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000002c",
              "role": "user",
              "parts": [
                {
                  "type": "text",
                  "text": "What can I make with leftover chicken?"
                }
              ]
            }
          ]
        }
      },
      "ChatMessageInput": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 4000,
            "default": "",
            "description": "What they say. Can be left out with files."
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "pattern": "^[a-f0-9]{24}$"
                },
                "name": {
                  "type": "string",
                  "maxLength": 200,
                  "description": "Its name, to show and to tell the model: \"lasagna.pdf\"."
                }
              },
              "required": [
                "id"
              ]
            },
            "maxItems": 4,
            "description": "Photos and PDFs to send with it, up to 4, by the ids `POST /assistant/files` gave them: a photo of what they're cooking, or a recipe to draft. The message keeps them as file parts, linking to each file."
          },
          "context": {
            "type": "object",
            "properties": {
              "recipe": {
                "type": "string",
                "description": "The recipe on their screen, by id or address."
              },
              "week": {
                "type": "string",
                "description": "The first day of the week of the plan on their screen."
              }
            },
            "description": "What's on their screen, for the assistant to know: \"this recipe\" is the one there."
          }
        },
        "example": {
          "text": "Is this thick enough?",
          "files": [
            {
              "id": "4f1c2a9e7b3d5f6a8c0e1b2d",
              "name": "sauce.jpg"
            }
          ],
          "context": {
            "recipe": "0199e8b0-5a10-7a01-8b00-000000000006"
          }
        }
      },
      "AssistantFile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Where it's shown from."
          },
          "mediaType": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "application/pdf"
            ],
            "description": "A photo is kept as JPEG; a PDF as it is."
          }
        },
        "required": [
          "id",
          "url",
          "mediaType"
        ],
        "example": {
          "id": "4f1c2a9e7b3d5f6a8c0e1b2d",
          "url": "/api/assistant/files/4f1c2a9e7b3d5f6a8c0e1b2d",
          "mediaType": "image/jpeg"
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "example": "mrn_3fK9aQ"
          },
          "household": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ]
          },
          "permissions": {
            "$ref": "#/components/schemas/ApiKeyPermissions"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "start",
          "household",
          "permissions",
          "expiresAt",
          "lastUsedAt",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000009",
          "name": "Home Assistant",
          "start": "mrn_3fK9aQ",
          "household": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000003",
            "name": "The Riveras"
          },
          "permissions": {
            "recipes": "read",
            "list": "write"
          },
          "expiresAt": "2027-01-13T18:30:00.000Z",
          "lastUsedAt": "2026-10-15T18:30:00.000Z",
          "createdAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "ApiKeyPermissions": {
        "type": "object",
        "properties": {
          "household": {
            "type": "string",
            "enum": [
              "read"
            ]
          },
          "recipes": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          },
          "plan": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          },
          "list": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          },
          "pantry": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          },
          "imports": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          },
          "assistant": {
            "type": "string",
            "enum": [
              "read",
              "write"
            ]
          }
        },
        "additionalProperties": false,
        "example": {
          "recipes": "read",
          "list": "write"
        }
      },
      "NewApiKey": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "example": "mrn_3fK9aQ…"
              }
            },
            "required": [
              "key"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000009",
          "name": "Home Assistant",
          "start": "mrn_3fK9aQ",
          "household": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000003",
            "name": "The Riveras"
          },
          "permissions": {
            "recipes": "read",
            "list": "write"
          },
          "expiresAt": "2027-01-13T18:30:00.000Z",
          "lastUsedAt": null,
          "createdAt": "2026-10-01T17:30:00.000Z",
          "key": "mrn_3fK9aQ…"
        }
      },
      "NewApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "permissions": {
            "$ref": "#/components/schemas/ApiKeyPermissions"
          },
          "household": {
            "type": "string",
            "description": "Which of your households it's for: the one in use when left out."
          },
          "expiresInDays": {
            "anyOf": [
              {
                "type": "number",
                "enum": [
                  30
                ]
              },
              {
                "type": "number",
                "enum": [
                  90
                ]
              },
              {
                "type": "number",
                "enum": [
                  365
                ]
              },
              {
                "type": "null"
              }
            ],
            "default": 90
          }
        },
        "required": [
          "name",
          "permissions"
        ],
        "example": {
          "name": "Home Assistant",
          "permissions": {
            "recipes": "read",
            "list": "write"
          },
          "expiresInDays": 90
        }
      },
      "Invite": {
        "type": "object",
        "properties": {
          "household": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              }
            },
            "required": [
              "name"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "household",
          "role",
          "expiresAt"
        ],
        "example": {
          "household": {
            "name": "The Riveras"
          },
          "role": "member",
          "expiresAt": "2026-10-22T18:30:00.000Z"
        }
      },
      "Me": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "serverAdmin": {
            "type": "boolean"
          },
          "households": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "role": {
                  "type": "string",
                  "enum": [
                    "owner",
                    "admin",
                    "member"
                  ]
                }
              },
              "required": [
                "id",
                "name",
                "role"
              ]
            }
          },
          "activeHouseholdId": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "username",
          "serverAdmin",
          "households",
          "activeHouseholdId"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000001",
          "name": "Sam Rivera",
          "email": "sam@example.com",
          "username": "sam",
          "serverAdmin": true,
          "households": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000003",
              "name": "The Riveras",
              "role": "owner"
            }
          ],
          "activeHouseholdId": "0199e8b0-5a10-7a01-8b00-000000000003"
        }
      },
      "JoinRequest": {
        "type": "object",
        "properties": {
          "account": {
            "$ref": "#/components/schemas/NewAccount"
          }
        },
        "example": {
          "account": {
            "name": "Jamie Rivera",
            "email": "jamie@example.com",
            "username": "jamie",
            "password": "correct horse battery staple"
          }
        }
      },
      "NewAccount": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "username": {
            "type": "string",
            "minLength": 3,
            "maxLength": 30,
            "pattern": "^[a-zA-Z0-9_.]+$"
          },
          "password": {
            "type": "string",
            "minLength": 10,
            "maxLength": 128
          }
        },
        "required": [
          "name",
          "email",
          "username",
          "password"
        ],
        "example": {
          "name": "Jamie Rivera",
          "email": "jamie@example.com",
          "username": "jamie",
          "password": "correct horse battery staple"
        }
      },
      "MyData": {
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "merran.person"
            ]
          },
          "version": {
            "type": "number"
          },
          "exportedAt": {
            "type": "string",
            "format": "date-time"
          },
          "account": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "username": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "preferences": {
                "type": "object",
                "additionalProperties": {}
              }
            },
            "required": [
              "name",
              "email",
              "username",
              "createdAt",
              "preferences"
            ]
          },
          "households": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "role": {
                  "type": "string"
                },
                "joinedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "required": [
                "name",
                "role",
                "joinedAt"
              ]
            }
          },
          "cookingNotes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "household": {
                  "type": "string"
                },
                "recipe": {
                  "type": "string"
                },
                "rating": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "note": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "required": [
                "household",
                "recipe",
                "rating",
                "note",
                "createdAt"
              ]
            }
          },
          "apiKeys": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "household": {
                  "type": "string"
                },
                "permissions": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                },
                "lastUsedAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "expiresAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              },
              "required": [
                "name",
                "household",
                "permissions",
                "createdAt",
                "lastUsedAt",
                "expiresAt"
              ]
            }
          }
        },
        "required": [
          "format",
          "version",
          "exportedAt",
          "account",
          "households",
          "cookingNotes",
          "apiKeys"
        ],
        "example": {
          "format": "merran.person",
          "version": 1,
          "exportedAt": "2026-10-15T18:30:00.000Z",
          "account": {
            "name": "Sam Rivera",
            "email": "sam@example.com",
            "username": "sam",
            "createdAt": "2026-10-01T17:30:00.000Z",
            "preferences": {}
          },
          "households": [
            {
              "name": "The Riveras",
              "role": "owner",
              "joinedAt": "2026-10-01T17:30:00.000Z"
            }
          ],
          "cookingNotes": [
            {
              "household": "The Riveras",
              "recipe": "Lemon Chicken",
              "rating": 5,
              "note": "More lemon next time.",
              "createdAt": "2026-10-09T01:15:00.000Z"
            }
          ],
          "apiKeys": [
            {
              "name": "Home Assistant",
              "household": "The Riveras",
              "permissions": {
                "recipes": "read",
                "list": "write"
              },
              "createdAt": "2026-10-01T17:30:00.000Z",
              "lastUsedAt": "2026-10-15T18:30:00.000Z",
              "expiresAt": "2027-01-13T18:30:00.000Z"
            }
          ]
        }
      },
      "SetupStatus": {
        "type": "object",
        "properties": {
          "needed": {
            "type": "boolean"
          }
        },
        "required": [
          "needed"
        ],
        "example": {
          "needed": true
        }
      },
      "SetupRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/NewAccount"
          },
          {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "minLength": 1
              },
              "household": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100
              }
            },
            "required": [
              "code",
              "household"
            ]
          }
        ],
        "example": {
          "name": "Sam Rivera",
          "email": "sam@example.com",
          "username": "sam",
          "password": "correct horse battery staple",
          "code": "ABCD-EFGH-JKMN",
          "household": "The Riveras"
        }
      },
      "Capabilities": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string"
          },
          "modules": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "example": "errorTracking"
                },
                "on": {
                  "type": "boolean"
                },
                "needs": {
                  "type": "string",
                  "description": "When it's off, what it needs, as a stable code.",
                  "example": "dsn"
                }
              },
              "required": [
                "id",
                "on"
              ]
            }
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What the modules that are on make possible."
          },
          "client": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {}
            },
            "description": "What the web app needs from each module that's on, by module id."
          }
        },
        "required": [
          "version",
          "modules",
          "capabilities",
          "client"
        ],
        "example": {
          "version": "0.1.0",
          "modules": [
            {
              "id": "errorTracking",
              "on": true
            },
            {
              "id": "telemetry",
              "on": true
            }
          ],
          "capabilities": [],
          "client": {
            "errorTracking": {
              "dsn": "https://public-key@errors.example.com/1",
              "environment": "home",
              "sampleRate": 1,
              "release": "0.1.0"
            }
          }
        }
      },
      "CollectionList": {
        "type": "object",
        "properties": {
          "collections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CollectionSummary"
            }
          }
        },
        "required": [
          "collections"
        ],
        "example": {
          "collections": [
            {
              "id": "weeknight",
              "title": "Weeknight wins",
              "description": "Dinners on the table in 40 minutes or less.",
              "theme": "Weeknight dinners on the table in 40 minutes or less",
              "count": 2,
              "toImport": 1,
              "photos": [
                "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo"
              ]
            }
          ]
        }
      },
      "CollectionSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "theme": {
            "type": "string",
            "description": "What Find more looks for."
          },
          "count": {
            "type": "number"
          },
          "toImport": {
            "type": "number",
            "description": "How many aren't in the library yet."
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to four, for its card."
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "theme",
          "count",
          "toImport",
          "photos"
        ],
        "example": {
          "id": "weeknight",
          "title": "Weeknight wins",
          "description": "Dinners on the table in 40 minutes or less.",
          "theme": "Weeknight dinners on the table in 40 minutes or less",
          "count": 2,
          "toImport": 1,
          "photos": [
            "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo"
          ]
        }
      },
      "Collection": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "theme": {
            "type": "string",
            "description": "What Find more looks for."
          },
          "count": {
            "type": "number"
          },
          "toImport": {
            "type": "number",
            "description": "How many aren't in the library yet."
          },
          "photos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Up to four, for its card."
          },
          "recipes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ShelfRecipe"
            },
            "description": "Up to 24 that suit the household, those not in its library first: the sites' newest that fit, its hand-picked classics, then the rest."
          },
          "found": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Discovery"
              },
              {
                "type": "null"
              }
            ],
            "description": "What Find more found for it last, or is finding."
          }
        },
        "required": [
          "id",
          "title",
          "description",
          "theme",
          "count",
          "toImport",
          "photos",
          "recipes",
          "found"
        ],
        "example": {
          "id": "weeknight",
          "title": "Weeknight wins",
          "description": "Dinners on the table in 40 minutes or less.",
          "theme": "Weeknight dinners on the table in 40 minutes or less",
          "count": 2,
          "toImport": 1,
          "photos": [
            "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo"
          ],
          "recipes": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000031",
              "url": "https://www.recipetineats.com/honey-garlic-chicken/",
              "name": "Honey Garlic Chicken",
              "site": "RecipeTin Eats",
              "description": "Juicy chicken in a sticky honey garlic sauce, in 12 minutes.",
              "minutes": 12,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": null,
              "suits": true
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000032",
              "url": "https://www.recipetineats.com/lemon-chicken/",
              "name": "Lemon Chicken",
              "site": "RecipeTin Eats",
              "description": null,
              "minutes": 35,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": {
                "slug": "lemon-chicken",
                "name": "Lemon Chicken"
              },
              "suits": true
            }
          ],
          "found": null
        }
      },
      "ShelfRecipe": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The recipe's page, to import at `POST /recipes/import`."
          },
          "name": {
            "type": "string"
          },
          "site": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "The recipe's own short description, when the site gives one."
          },
          "minutes": {
            "type": [
              "number",
              "null"
            ]
          },
          "courses": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "main",
                "side",
                "soup",
                "salad",
                "dessert",
                "baking",
                "breakfast",
                "snack",
                "drink",
                "sauce"
              ]
            },
            "description": "What its site says it is, in Merran's words; empty when it doesn't say."
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "A small copy of its photo, through Merran."
          },
          "publishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "inLibrary": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "slug",
              "name"
            ],
            "description": "The household's recipe from it, if it has one."
          },
          "suits": {
            "type": "boolean",
            "description": "False when it has a food the household avoids, or is off its diets."
          }
        },
        "required": [
          "id",
          "url",
          "name",
          "site",
          "description",
          "minutes",
          "courses",
          "photo",
          "publishedAt",
          "inLibrary",
          "suits"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000031",
          "url": "https://www.recipetineats.com/honey-garlic-chicken/",
          "name": "Honey Garlic Chicken",
          "site": "RecipeTin Eats",
          "description": "Juicy chicken in a sticky honey garlic sauce, in 12 minutes.",
          "minutes": 12,
          "courses": [
            "main"
          ],
          "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
          "publishedAt": "2026-10-01T17:30:00.000Z",
          "inLibrary": null,
          "suits": true
        }
      },
      "Discovery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "theme": {
            "type": "string"
          },
          "collection": {
            "type": [
              "string",
              "null"
            ],
            "description": "The starter collection it found more for (`POST /collections/{collection}/find`), or null."
          },
          "status": {
            "type": "string",
            "enum": [
              "searching",
              "done",
              "failed"
            ],
            "description": "Still searching, done, or failed (`error` says why)."
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "recipes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string",
                  "description": "The recipe's page, to import at `POST /recipes/import`."
                },
                "name": {
                  "type": "string"
                },
                "site": {
                  "type": "string"
                },
                "minutes": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "photo": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "why": {
                  "type": "string",
                  "description": "Why it's a good pick."
                }
              },
              "required": [
                "url",
                "name",
                "site",
                "minutes",
                "photo",
                "why"
              ]
            },
            "description": "Up to eight, each a page Merran opened and can import, no more than two from a site unless there aren't others."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "theme",
          "collection",
          "status",
          "error",
          "recipes",
          "createdAt",
          "updatedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002d",
          "theme": "cozy fall soups",
          "collection": null,
          "status": "done",
          "error": null,
          "recipes": [
            {
              "url": "https://cooking.example/butternut-squash-soup",
              "name": "Roasted Butternut Squash Soup",
              "site": "cooking.example",
              "minutes": 60,
              "photo": "https://cooking.example/images/squash-soup.jpg",
              "why": "Vegetarian and freezes well, with the squash roasted for depth."
            }
          ],
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "LinkImport": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The import's job."
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "tag": {
            "type": [
              "string",
              "null"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "added": {
            "type": "integer"
          },
          "already": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "stopped": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "state": {
                  "type": "string",
                  "enum": [
                    "waiting",
                    "adding",
                    "added",
                    "already",
                    "failed",
                    "stopped"
                  ],
                  "description": "Where the link has got to: added, already in the library, failed (see error), or stopped before it was reached."
                },
                "recipe": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "photo": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "slug",
                    "name",
                    "photo"
                  ],
                  "description": "The recipe it made, or was already."
                },
                "error": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "url",
                "state",
                "recipe",
                "error"
              ]
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "tag",
          "total",
          "done",
          "added",
          "already",
          "failed",
          "stopped",
          "items",
          "createdAt",
          "finishedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
          "status": "running",
          "tag": "to try",
          "total": 2,
          "done": 1,
          "added": 1,
          "already": 0,
          "failed": 0,
          "stopped": 0,
          "items": [
            {
              "url": "https://example.com/garlic-rice",
              "state": "added",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice",
                "photo": null
              },
              "error": null
            },
            {
              "url": "https://example.com/lemon-chicken",
              "state": "adding",
              "recipe": null,
              "error": null
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z",
          "finishedAt": null
        }
      },
      "DiscoveryList": {
        "type": "object",
        "properties": {
          "searches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Discovery"
            }
          }
        },
        "required": [
          "searches"
        ],
        "example": {
          "searches": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000002d",
              "theme": "cozy fall soups",
              "collection": null,
              "status": "done",
              "error": null,
              "recipes": [
                {
                  "url": "https://cooking.example/butternut-squash-soup",
                  "name": "Roasted Butternut Squash Soup",
                  "site": "cooking.example",
                  "minutes": 60,
                  "photo": "https://cooking.example/images/squash-soup.jpg",
                  "why": "Vegetarian and freezes well, with the squash roasted for depth."
                }
              ],
              "createdAt": "2026-10-01T17:30:00.000Z",
              "updatedAt": "2026-10-01T17:30:00.000Z"
            }
          ]
        }
      },
      "DiscoverInput": {
        "type": "object",
        "properties": {
          "theme": {
            "type": "string",
            "minLength": 2,
            "maxLength": 200
          }
        },
        "required": [
          "theme"
        ],
        "example": {
          "theme": "cozy fall soups"
        }
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "version": {
            "type": "string",
            "example": "0.1.0"
          }
        },
        "required": [
          "status",
          "version"
        ],
        "example": {
          "status": "ok",
          "version": "0.1.0"
        }
      },
      "Ready": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ready"
            ]
          },
          "database": {
            "type": "string",
            "enum": [
              "sqlite",
              "postgres"
            ]
          }
        },
        "required": [
          "status",
          "database"
        ],
        "example": {
          "status": "ready",
          "database": "sqlite"
        }
      },
      "Household": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "timeZone": {
            "type": "string"
          },
          "weekStartsOn": {
            "type": "integer"
          },
          "meals": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "breakfast",
                "lunch",
                "dinner",
                "snack"
              ]
            },
            "description": "The meals filled for it (by Fill the week and Plan each week), in a day's order: every meal until it opts out of some. Any meal can be planned by hand."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "country",
          "currency",
          "timeZone",
          "weekStartsOn",
          "meals",
          "role"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000003",
          "name": "The Riveras",
          "country": "US",
          "currency": "USD",
          "timeZone": "America/Phoenix",
          "weekStartsOn": 0,
          "meals": [
            "dinner"
          ],
          "role": "owner"
        }
      },
      "HouseholdChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Z]{2}$"
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Z]{3}$"
          },
          "timeZone": {
            "type": "string"
          },
          "weekStartsOn": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6
          },
          "meals": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "breakfast",
                "lunch",
                "dinner",
                "snack"
              ]
            },
            "minItems": 1,
            "description": "The meals filled for it."
          }
        },
        "example": {
          "name": "The Rivera-Parks",
          "weekStartsOn": 1
        }
      },
      "HouseholdAi": {
        "type": "object",
        "properties": {
          "status": {
            "type": "object",
            "properties": {
              "available": {
                "type": "boolean"
              },
              "tasks": {
                "type": "object",
                "properties": {
                  "assistant": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "import": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "tidy": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "notes": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "swaps": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "nutrition": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "shopping": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "receipts": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "discover": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  },
                  "reminders": {
                    "anyOf": [
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              true
                            ]
                          },
                          "ref": {
                            "type": "string"
                          },
                          "source": {
                            "type": "string",
                            "enum": [
                              "server",
                              "household"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "ref",
                          "source"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "ok": {
                            "type": "boolean",
                            "enum": [
                              false
                            ]
                          },
                          "why": {
                            "type": "string",
                            "enum": [
                              "noKey",
                              "notCovered",
                              "overLimit",
                              "ownLimit"
                            ]
                          }
                        },
                        "required": [
                          "ok",
                          "why"
                        ]
                      }
                    ]
                  }
                },
                "description": "For each task, the model and whose key, or why none."
              },
              "month": {
                "type": "string"
              },
              "server": {
                "type": "object",
                "properties": {
                  "covers": {
                    "type": "boolean"
                  },
                  "hasKeys": {
                    "type": "boolean"
                  },
                  "limit": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "In US dollars."
                  },
                  "spent": {
                    "type": "number",
                    "description": "In US dollars."
                  },
                  "near": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "covers",
                  "hasKeys",
                  "limit",
                  "spent",
                  "near"
                ]
              },
              "own": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "In US dollars."
                  },
                  "spent": {
                    "type": "number",
                    "description": "In US dollars."
                  },
                  "near": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "limit",
                  "spent",
                  "near"
                ]
              },
              "mayAddKeys": {
                "type": "boolean"
              },
              "mayAddAddresses": {
                "type": "boolean"
              },
              "keyAlerts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "source": {
                      "type": "string",
                      "enum": [
                        "server",
                        "household"
                      ]
                    },
                    "provider": {
                      "type": "string",
                      "enum": [
                        "anthropic",
                        "openai",
                        "google",
                        "openai-compatible"
                      ]
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "outOfCredit": {
                      "type": "boolean"
                    },
                    "left": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "About what's left, when credit was entered."
                    }
                  },
                  "required": [
                    "id",
                    "source",
                    "provider",
                    "name",
                    "outOfCredit",
                    "left"
                  ]
                },
                "description": "The keys it relies on, its own and the server's, that are out of credit or running low."
              }
            },
            "required": [
              "available",
              "tasks",
              "month",
              "server",
              "own",
              "mayAddKeys",
              "mayAddAddresses",
              "keyAlerts"
            ]
          },
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiKey"
            }
          },
          "settings": {
            "$ref": "#/components/schemas/HouseholdAiSettings"
          },
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiModelOption"
            }
          },
          "usage": {
            "type": "object",
            "properties": {
              "months": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "month": {
                      "type": "string"
                    },
                    "server": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "household": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "calls": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "month",
                    "server",
                    "household",
                    "calls"
                  ]
                }
              },
              "tasks": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "task": {
                      "type": "string"
                    },
                    "cost": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "calls": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "task",
                    "cost",
                    "calls"
                  ]
                }
              },
              "people": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "userId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "cost": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "calls": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "userId",
                    "name",
                    "cost",
                    "calls"
                  ]
                }
              }
            },
            "required": [
              "months",
              "tasks",
              "people"
            ]
          }
        },
        "required": [
          "status",
          "keys",
          "settings",
          "models",
          "usage"
        ],
        "example": {
          "status": {
            "available": true,
            "tasks": {
              "assistant": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "import": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "tidy": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "notes": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "swaps": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "nutrition": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "shopping": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "receipts": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "discover": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              },
              "reminders": {
                "ok": true,
                "ref": "anthropic:claude-opus-5-5",
                "source": "server"
              }
            },
            "month": "2026-10",
            "server": {
              "covers": true,
              "hasKeys": true,
              "limit": 5,
              "spent": 1.24,
              "near": false
            },
            "own": {
              "limit": null,
              "spent": 0,
              "near": false
            },
            "mayAddKeys": true,
            "mayAddAddresses": false,
            "keyAlerts": []
          },
          "keys": [],
          "settings": {
            "large": null,
            "small": null,
            "tasks": {},
            "monthlyLimit": null
          },
          "models": [],
          "usage": {
            "months": [
              {
                "month": "2026-10",
                "server": 1.24,
                "household": 0,
                "calls": 31
              }
            ],
            "tasks": [
              {
                "task": "import",
                "cost": 1.24,
                "calls": 31
              }
            ],
            "people": [
              {
                "userId": "0199e8b0-5a10-7a01-8b00-000000000001",
                "name": "Sam",
                "cost": 1.24,
                "calls": 31
              }
            ]
          }
        }
      },
      "AiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Its id; \"env:anthropic\" and the like for a key set in the server's environment."
          },
          "provider": {
            "type": "string",
            "enum": [
              "anthropic",
              "openai",
              "google",
              "openai-compatible"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "An address's name."
          },
          "baseUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "An OpenAI-compatible address."
          },
          "hint": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key's last four characters. The key itself is never sent."
          },
          "models": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "description": "What it can use, from its last test."
          },
          "testedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "fromEnvironment": {
            "type": "boolean",
            "description": "Set in the server's environment, so it can't be changed here."
          },
          "unreadable": {
            "type": "boolean",
            "description": "Sealed with a master key the server no longer has: take it off and add it again."
          },
          "credit": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "amount": {
                "type": "number",
                "description": "The credit entered, in US dollars."
              },
              "since": {
                "type": "string",
                "format": "date-time",
                "description": "When it was entered."
              },
              "spent": {
                "type": "number",
                "description": "What Merran has spent on the key since."
              },
              "left": {
                "type": "number",
                "description": "About what's left. Use of the key outside Merran isn't counted."
              }
            },
            "required": [
              "amount",
              "since",
              "spent",
              "left"
            ],
            "description": "The providers don't tell a key how much credit it has left, so it's what someone entered, less what Merran's spent on it since. None until entered."
          },
          "outOfCreditAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When its provider last said it's out of credit. It's passed over for an hour, then tried again; it clears once it works."
          },
          "lowOnCredit": {
            "type": "boolean",
            "description": "About a fifth or less of its entered credit left."
          },
          "creditCheck": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "result": {
                "type": "string",
                "enum": [
                  "ok",
                  "out",
                  "failed"
                ]
              },
              "message": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "result",
              "message"
            ],
            "description": "Only from adding or testing a key: whether one tiny call on its cheapest model found credit (`ok`), none (`out`), or couldn't tell (`failed`, with the provider's words). Addresses aren't checked."
          }
        },
        "required": [
          "id",
          "provider",
          "name",
          "baseUrl",
          "hint",
          "models",
          "testedAt",
          "fromEnvironment",
          "unreadable",
          "credit",
          "outOfCreditAt",
          "lowOnCredit"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000028",
          "provider": "anthropic",
          "name": null,
          "baseUrl": null,
          "hint": "7xQa",
          "models": [
            "claude-haiku-5-5",
            "claude-opus-5-5"
          ],
          "testedAt": "2026-10-16T17:00:00.000Z",
          "fromEnvironment": false,
          "unreadable": false,
          "credit": {
            "amount": 20,
            "since": "2026-10-01T17:00:00.000Z",
            "spent": 16.6,
            "left": 3.4
          },
          "outOfCreditAt": null,
          "lowOnCredit": true
        }
      },
      "HouseholdAiSettings": {
        "type": "object",
        "properties": {
          "large": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300,
            "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
            "example": "anthropic:claude-opus-5-5"
          },
          "small": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300,
            "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
            "example": "anthropic:claude-opus-5-5"
          },
          "tasks": {
            "type": "object",
            "properties": {
              "assistant": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "import": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "tidy": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "notes": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "swaps": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "nutrition": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "shopping": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "receipts": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "discover": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              },
              "reminders": {
                "type": "string",
                "maxLength": 300,
                "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
                "example": "anthropic:claude-opus-5-5"
              }
            },
            "description": "A model for a task of its own."
          },
          "monthlyLimit": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000,
            "description": "A monthly limit on its own keys, in US dollars; null for none."
          },
          "autoTidy": {
            "type": "boolean",
            "default": false,
            "description": "Tidy up each recipe it imports, in the background, with the suggestions left in the Review queue."
          }
        },
        "required": [
          "large",
          "small",
          "tasks",
          "monthlyLimit"
        ],
        "example": {
          "large": null,
          "small": null,
          "tasks": {},
          "monthlyLimit": 10,
          "autoTidy": false
        }
      },
      "AiModelOption": {
        "type": "object",
        "properties": {
          "ref": {
            "type": "string",
            "maxLength": 300,
            "description": "A model: \"anthropic:claude-opus-5-5\", or on an address, \"endpoint:<its id>:llama3.1\".",
            "example": "anthropic:claude-opus-5-5"
          },
          "name": {
            "type": "string"
          },
          "size": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "large",
              "small",
              null
            ]
          },
          "price": {
            "type": [
              "array",
              "null"
            ],
            "prefixItems": [
              {
                "type": "number"
              },
              {
                "type": "number"
              }
            ],
            "description": "US dollars per million tokens, in and out; null when unknown, or free on an address."
          },
          "source": {
            "type": "string",
            "enum": [
              "server",
              "household"
            ],
            "description": "Whose key runs it."
          }
        },
        "required": [
          "ref",
          "name",
          "size",
          "price",
          "source"
        ]
      },
      "NewAiKey": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string",
                "enum": [
                  "anthropic",
                  "openai",
                  "google"
                ]
              },
              "key": {
                "type": "string",
                "minLength": 10,
                "maxLength": 500
              }
            },
            "required": [
              "provider",
              "key"
            ]
          },
          {
            "type": "object",
            "properties": {
              "provider": {
                "type": "string",
                "enum": [
                  "openai-compatible"
                ]
              },
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 60
              },
              "baseUrl": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500
              },
              "key": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 500
              }
            },
            "required": [
              "provider",
              "name",
              "baseUrl"
            ]
          }
        ],
        "description": "A provider's key, or an OpenAI-compatible address (an Ollama, LM Studio, OpenRouter and the like) with a key or none. It's tested before it's kept.",
        "example": {
          "provider": "anthropic",
          "key": "sk-ant-api03-...7xQa"
        }
      },
      "KeyCredit": {
        "type": "object",
        "properties": {
          "amount": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000,
            "description": "The credit on the key now, in US dollars; null to stop counting."
          }
        },
        "required": [
          "amount"
        ],
        "example": {
          "amount": 20
        }
      },
      "Job": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "description": "What it does, like \"household.import\".",
            "example": "household.import"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "scheduled": {
            "type": "boolean",
            "description": "Whether it comes round on a schedule, like the nightly purge."
          },
          "householdId": {
            "type": [
              "string",
              "null"
            ]
          },
          "progress": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "done": {
                "type": "number"
              },
              "total": {
                "type": "number"
              }
            },
            "required": [
              "done",
              "total"
            ],
            "description": "How far it's got, once it says."
          },
          "result": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {},
            "description": "What it did, once it's done. A job followed item by item, like importing several links, says what it's done so far as it goes."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it last failed."
          },
          "attempts": {
            "type": "number"
          },
          "maxAttempts": {
            "type": "number"
          },
          "runAt": {
            "type": "string",
            "format": "date-time",
            "description": "Not before then."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "kind",
          "status",
          "scheduled",
          "householdId",
          "progress",
          "result",
          "error",
          "attempts",
          "maxAttempts",
          "runAt",
          "startedAt",
          "finishedAt",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
          "kind": "household.import",
          "status": "running",
          "scheduled": false,
          "householdId": "0199e8b0-5a10-7a01-8b00-000000000003",
          "progress": {
            "done": 42,
            "total": 128
          },
          "result": null,
          "error": null,
          "attempts": 1,
          "maxAttempts": 3,
          "runAt": "2026-10-15T18:30:00.000Z",
          "startedAt": "2026-10-15T18:30:00.000Z",
          "finishedAt": null,
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ImportFile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Start its import at /households/current/import/files/{fileId}/start."
          },
          "app": {
            "type": "string",
            "enum": [
              "mealie",
              "paprika"
            ]
          },
          "what": {
            "type": "string",
            "enum": [
              "backup",
              "recipes"
            ],
            "description": "A whole backup, or recipes exported from the app."
          },
          "recipes": {
            "type": "integer"
          },
          "alreadyHere": {
            "type": "integer",
            "description": "Of those, how many an earlier import brought in. They're skipped."
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "recipes": {
                  "type": "integer"
                }
              },
              "required": [
                "id",
                "name",
                "recipes"
              ]
            },
            "description": "A Mealie backup's groups, when there's more than one to choose from."
          }
        },
        "required": [
          "id",
          "app",
          "what",
          "recipes",
          "alreadyHere",
          "groups"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000018",
          "app": "mealie",
          "what": "backup",
          "recipes": 312,
          "alreadyHere": 0,
          "groups": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000019",
              "name": "Home",
              "recipes": 298
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001a",
              "name": "Grandma",
              "recipes": 14
            }
          ]
        }
      },
      "StartImportFile": {
        "type": "object",
        "properties": {
          "group": {
            "type": [
              "string",
              "null"
            ],
            "description": "Which of a Mealie backup's groups to import. All of them if left out."
          }
        },
        "example": {
          "group": "0199e8b0-5a10-7a01-8b00-000000000019"
        }
      },
      "MealieCheck": {
        "type": "object",
        "properties": {
          "version": {
            "type": [
              "string",
              "null"
            ],
            "example": "v3.28.0"
          },
          "recipes": {
            "type": "integer"
          },
          "alreadyHere": {
            "type": "integer",
            "description": "Recipes an earlier Mealie import brought in. They're skipped."
          }
        },
        "required": [
          "version",
          "recipes",
          "alreadyHere"
        ],
        "example": {
          "version": "v3.28.0",
          "recipes": 312,
          "alreadyHere": 0
        }
      },
      "MealieConnection": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 2000,
            "format": "uri"
          },
          "token": {
            "type": "string",
            "minLength": 10,
            "maxLength": 4000
          }
        },
        "required": [
          "url",
          "token"
        ],
        "example": {
          "url": "http://192.168.1.20:9000",
          "token": "sample-mealie-api-token"
        }
      },
      "InviteLink": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "member"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "role",
          "expiresAt",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000a",
          "role": "member",
          "expiresAt": "2026-10-22T18:30:00.000Z",
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "NewInviteLink": {
        "allOf": [
          {
            "$ref": "#/components/schemas/InviteLink"
          },
          {
            "type": "object",
            "properties": {
              "link": {
                "type": "string",
                "format": "uri"
              }
            },
            "required": [
              "link"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000a",
          "role": "member",
          "expiresAt": "2026-10-22T18:30:00.000Z",
          "createdAt": "2026-10-15T18:30:00.000Z",
          "link": "https://merran.example.com/join/sample-invite-token"
        }
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member"
            ]
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "userId",
          "name",
          "username",
          "role",
          "joinedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000005",
          "userId": "0199e8b0-5a10-7a01-8b00-000000000002",
          "name": "Jamie Rivera",
          "username": "jamie",
          "role": "admin",
          "joinedAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "OnHand": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "maxItems": 100,
            "description": "Left off lists made from the plan. Each covers its kinds: \"salt\" is kosher and sea salt too."
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            "salt",
            "pepper",
            "water",
            "ice",
            "olive oil"
          ]
        }
      },
      "Tastes": {
        "type": "object",
        "properties": {
          "avoid": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "food": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 60
                },
                "level": {
                  "type": "string",
                  "enum": [
                    "allergy",
                    "never",
                    "leave-out"
                  ],
                  "description": "How firmly: an `allergy` or `never`, and recipes with it aren't suggested; `leave-out`, and they are, with it crossed off, unless it's what the dish is or the recipe needs it. Nuts, dairy, gluten, eggs and meat can't be left out, so they're saved as `never`."
                },
                "who": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 40,
                  "description": "Who doesn't eat it, when it isn't everyone."
                }
              },
              "required": [
                "food",
                "level"
              ]
            },
            "maxItems": 100,
            "description": "Foods the household doesn't eat."
          },
          "diets": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "vegetarian",
                "vegan",
                "gluten-free",
                "dairy-free",
                "egg-free",
                "nut-free"
              ]
            },
            "maxItems": 6,
            "description": "Diets every meal must fit."
          },
          "weeknightMinutes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 10,
            "maximum": 240,
            "description": "The longest a dinner Monday to Thursday can take."
          }
        },
        "required": [
          "avoid",
          "diets",
          "weeknightMinutes"
        ],
        "example": {
          "avoid": [
            {
              "food": "peanuts",
              "level": "allergy",
              "who": "Sam"
            },
            {
              "food": "mushrooms",
              "level": "leave-out",
              "who": "Emma"
            }
          ],
          "diets": [],
          "weeknightMinutes": 45
        }
      },
      "ThemeNights": {
        "type": "object",
        "properties": {
          "themes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NightTheme"
            },
            "description": "Dinner's theme each weekday, Sunday first; null for none."
          }
        },
        "required": [
          "themes"
        ],
        "example": {
          "themes": [
            null,
            null,
            {
              "kind": "tag",
              "name": "Tacos",
              "tag": "taco"
            },
            null,
            null,
            {
              "kind": "fish",
              "name": "Fish"
            },
            {
              "kind": "skip",
              "name": "Eat out"
            }
          ]
        }
      },
      "NightTheme": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "tag",
              "meatless",
              "fish",
              "new",
              "quick",
              "skip"
            ]
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "tag": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          }
        },
        "required": [
          "kind",
          "name"
        ]
      },
      "HouseholdImport": {
        "type": "object",
        "properties": {
          "household": {
            "$ref": "#/components/schemas/Household"
          },
          "job": {
            "$ref": "#/components/schemas/Job"
          }
        },
        "required": [
          "household",
          "job"
        ],
        "example": {
          "household": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000003",
            "name": "The Riveras",
            "country": "US",
            "currency": "USD",
            "timeZone": "America/Phoenix",
            "weekStartsOn": 0,
            "meals": [
              "dinner"
            ],
            "role": "owner"
          },
          "job": {
            "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
            "kind": "household.import",
            "status": "queued",
            "scheduled": false,
            "householdId": "0199e8b0-5a10-7a01-8b00-000000000003",
            "progress": null,
            "result": null,
            "error": null,
            "attempts": 0,
            "maxAttempts": 3,
            "runAt": "2026-10-15T18:30:00.000Z",
            "startedAt": null,
            "finishedAt": null,
            "createdAt": "2026-10-15T18:30:00.000Z"
          }
        }
      },
      "ShoppingList": {
        "type": "object",
        "properties": {
          "lists": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/List"
            },
            "description": "The household's lists, the main one first."
          },
          "aisles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Aisle"
            },
            "description": "In a walk through the store, the household's country's to start."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItem"
            },
            "description": "By aisle, in the aisles' order, oldest first in each."
          }
        },
        "required": [
          "lists",
          "aisles",
          "items"
        ],
        "example": {
          "lists": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "Shopping list",
              "main": true,
              "aisles": []
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000025",
              "name": "Costco",
              "main": false,
              "aisles": [
                "0199e8b0-5a10-7a01-8b00-000000000021"
              ]
            }
          ],
          "aisles": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000020",
              "name": "Produce",
              "position": 0
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000021",
              "name": "Meat & seafood",
              "position": 1
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000022",
              "name": "Other",
              "position": 2
            }
          ],
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001e",
              "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "lemons",
              "amount": "2",
              "quantity": 2,
              "unit": null,
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000020",
              "checked": false,
              "source": "plan",
              "meals": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "name": "Lemon Chicken"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "list": "0199e8b0-5a10-7a01-8b00-000000000025",
              "name": "chicken thighs",
              "amount": "3 lb",
              "quantity": 3,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "checked": false,
              "source": "plan",
              "meals": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "name": "Lemon Chicken"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "List": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "main": {
            "type": "boolean",
            "description": "The main list, which takes what no other list does."
          },
          "aisles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The ids of the aisles that go on it."
          }
        },
        "required": [
          "id",
          "name",
          "main",
          "aisles"
        ]
      },
      "Aisle": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "position": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "position"
        ]
      },
      "ListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "list": {
            "type": "string",
            "description": "The list it's on."
          },
          "name": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "description": "How much, as it shows: \"1 ½ cups\", \"half a bag\", or empty."
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "description": "A unit id, like \"cup\" or \"pound\"; null for a count."
          },
          "amountText": {
            "type": [
              "string",
              "null"
            ],
            "description": "An amount in words (\"half a bag\"), when it isn't a number."
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ],
            "description": "Its aisle's id."
          },
          "checked": {
            "type": "boolean"
          },
          "source": {
            "$ref": "#/components/schemas/ListItemSource"
          },
          "meals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "name"
              ]
            },
            "description": "The planned meals it's for."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "list",
          "name",
          "amount",
          "quantity",
          "unit",
          "amountText",
          "aisle",
          "checked",
          "source",
          "meals",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
          "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
          "name": "chicken thighs",
          "amount": "3 lb",
          "quantity": 3,
          "unit": "pound",
          "amountText": null,
          "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
          "checked": false,
          "source": "plan",
          "meals": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "name": "Lemon Chicken"
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ListItemSource": {
        "type": "string",
        "enum": [
          "plan",
          "typed",
          "every-week",
          "usual",
          "assistant"
        ],
        "description": "How it got on the list: from the plan, typed in, every week, a usual buy, or the assistant."
      },
      "ListBefore": {
        "type": "object",
        "properties": {
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItemSnapshot"
            }
          }
        },
        "required": [
          "before"
        ],
        "description": "What the items were before, for Undo with /list/restore.",
        "example": {
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 3,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "checked": false,
              "source": "plan",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 3,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "ListItemSnapshot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "list": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "foodKey": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "amountText": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          },
          "checked": {
            "type": "boolean"
          },
          "source": {
            "$ref": "#/components/schemas/ListItemSource"
          },
          "forMeals": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "properties": {
                "meal": {
                  "type": "string"
                },
                "quantity": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "unit": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "meal",
                "quantity",
                "unit"
              ]
            },
            "maxItems": 100
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "list",
          "name",
          "foodKey",
          "quantity",
          "unit",
          "amountText",
          "aisle",
          "checked",
          "source",
          "forMeals",
          "createdAt"
        ],
        "description": "An item as it was, to put it back (Undo).",
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
          "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
          "name": "chicken thighs",
          "foodKey": "chicken thigh",
          "quantity": 3,
          "unit": "pound",
          "amountText": null,
          "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
          "checked": false,
          "source": "plan",
          "forMeals": [
            {
              "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "quantity": 3,
              "unit": "pound"
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ClearList": {
        "type": "object",
        "properties": {
          "which": {
            "type": "string",
            "enum": [
              "checked",
              "all"
            ],
            "description": "What's checked off, or everything."
          }
        },
        "required": [
          "which"
        ],
        "example": {
          "which": "checked"
        }
      },
      "EveryWeek": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EveryWeekItem"
            }
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000023",
              "name": "Bananas",
              "amount": "1 bunch",
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000020"
            }
          ]
        }
      },
      "EveryWeekItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "amount",
          "aisle"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000023",
          "name": "Bananas",
          "amount": "1 bunch",
          "aisle": "0199e8b0-5a10-7a01-8b00-000000000020"
        }
      },
      "NewEveryWeekItem": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          }
        },
        "required": [
          "name"
        ],
        "example": {
          "name": "Bananas",
          "amount": "1 bunch"
        }
      },
      "EveryWeekChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          }
        },
        "example": {
          "amount": "2 bunches"
        }
      },
      "FoodAliases": {
        "type": "object",
        "properties": {
          "aliases": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FoodAlias"
            }
          }
        },
        "required": [
          "aliases"
        ],
        "example": {
          "aliases": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001f",
              "name": "unsalted butter",
              "food": "unsalted butter",
              "apart": true
            }
          ]
        }
      },
      "FoodAlias": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "A food's key made another's (Same as…), or a name kept its own (Not the same)."
          },
          "food": {
            "type": "string",
            "description": "The food it is, for the household."
          },
          "apart": {
            "type": "boolean",
            "description": "Kept its own food."
          }
        },
        "required": [
          "id",
          "name",
          "food",
          "apart"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000001f",
          "name": "unsalted butter",
          "food": "unsalted butter",
          "apart": true
        }
      },
      "FromPlanPreview": {
        "type": "object",
        "properties": {
          "meals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "date": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-15"
                },
                "anyDay": {
                  "type": "boolean"
                },
                "meal": {
                  "type": "string",
                  "enum": [
                    "breakfast",
                    "lunch",
                    "dinner",
                    "snack"
                  ]
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "date",
                "anyDay",
                "meal",
                "name"
              ]
            },
            "description": "Planned meals not on the list yet."
          },
          "listed": {
            "type": "integer",
            "description": "How many are on the list already."
          }
        },
        "required": [
          "meals",
          "listed"
        ],
        "example": {
          "meals": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "name": "Lemon Chicken"
            }
          ],
          "listed": 2
        }
      },
      "FromPlanResult": {
        "type": "object",
        "properties": {
          "meals": {
            "type": "integer",
            "description": "Meals added."
          },
          "added": {
            "type": "integer",
            "description": "New lines on the list."
          },
          "merged": {
            "type": "integer",
            "description": "Lines added to ones already there."
          },
          "everyWeek": {
            "type": "integer",
            "description": "Every-week items added."
          },
          "onHand": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Left off: always on hand."
          },
          "inPantry": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Left off: at home already, and now set aside for them."
          }
        },
        "required": [
          "meals",
          "added",
          "merged",
          "everyWeek",
          "onHand",
          "inPantry"
        ],
        "example": {
          "meals": 3,
          "added": 14,
          "merged": 2,
          "everyWeek": 2,
          "onHand": [
            "salt",
            "black pepper"
          ],
          "inPantry": [
            "rice"
          ]
        }
      },
      "FromPlan": {
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15"
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15"
          }
        },
        "required": [
          "from",
          "to"
        ],
        "example": {
          "from": "2026-10-12",
          "to": "2026-10-18"
        }
      },
      "NewListItem": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "\"2 lb ground beef\", or just \"paper towels\"."
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ],
            "description": "An aisle's id; the food's usual aisle when left out."
          },
          "list": {
            "type": [
              "string",
              "null"
            ],
            "description": "A list's id; the food's usual list when left out."
          }
        },
        "required": [
          "text"
        ],
        "example": {
          "text": "2 lb ground beef"
        }
      },
      "ListBulkResult": {
        "type": "object",
        "properties": {
          "changed": {
            "type": "integer"
          },
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItemSnapshot"
            }
          }
        },
        "required": [
          "changed",
          "before"
        ],
        "example": {
          "changed": 1,
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 3,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "checked": false,
              "source": "plan",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 3,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "ListBulk": {
        "type": "object",
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 500
          },
          "action": {
            "type": "string",
            "enum": [
              "remove",
              "check",
              "uncheck",
              "aisle",
              "move",
              "every-week"
            ]
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ],
            "description": "For aisle: the aisle's id, or null for none."
          },
          "list": {
            "type": "string",
            "description": "For move: the list's id."
          }
        },
        "required": [
          "ids",
          "action"
        ],
        "example": {
          "ids": [
            "0199e8b0-5a10-7a01-8b00-00000000001d",
            "0199e8b0-5a10-7a01-8b00-00000000001e"
          ],
          "action": "check"
        }
      },
      "ListItemChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": "string",
            "maxLength": 60,
            "description": "\"2 lb\", \"half a bag\", or empty for none."
          },
          "checked": {
            "type": "boolean"
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ],
            "description": "An aisle's id, or null. The household's remembered for the food: next time it goes there."
          },
          "list": {
            "type": "string",
            "description": "Another list's id. Remembered for the food too: next time it goes on that list."
          }
        },
        "example": {
          "checked": true
        }
      },
      "KeptApart": {
        "type": "object",
        "properties": {
          "kept": {
            "type": "integer"
          }
        },
        "required": [
          "kept"
        ],
        "example": {
          "kept": 1
        }
      },
      "KeepApart": {
        "type": "object",
        "properties": {
          "names": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "minItems": 1,
            "maxItems": 50
          }
        },
        "required": [
          "names"
        ],
        "example": {
          "names": [
            "unsalted butter"
          ]
        }
      },
      "JoinedNames": {
        "type": "object",
        "properties": {
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The names this line was joined from: its own, and its meals' lines that came to the same food.",
            "example": [
              "butter",
              "unsalted butter"
            ]
          }
        },
        "required": [
          "names"
        ],
        "example": {
          "names": [
            "butter",
            "unsalted butter"
          ]
        }
      },
      "SameAsResult": {
        "type": "object",
        "properties": {
          "food": {
            "type": "string",
            "description": "The food both are now, for the household.",
            "example": "butter"
          },
          "joined": {
            "type": "integer",
            "description": "How many lines joined into others.",
            "example": 1
          }
        },
        "required": [
          "food",
          "joined"
        ],
        "example": {
          "food": "butter",
          "joined": 1
        }
      },
      "SameAs": {
        "type": "object",
        "properties": {
          "item": {
            "type": "string",
            "minLength": 1,
            "description": "The other thing on the list it's the same food as.",
            "example": "0199e8b0-5a10-7a01-8b00-00000000001e"
          }
        },
        "required": [
          "item"
        ],
        "example": {
          "item": "0199e8b0-5a10-7a01-8b00-00000000001e"
        }
      },
      "Lists": {
        "type": "object",
        "properties": {
          "lists": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/List"
            }
          }
        },
        "required": [
          "lists"
        ],
        "example": {
          "lists": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "Shopping list",
              "main": true,
              "aisles": []
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000025",
              "name": "Costco",
              "main": false,
              "aisles": [
                "0199e8b0-5a10-7a01-8b00-000000000021"
              ]
            }
          ]
        }
      },
      "ListsInput": {
        "type": "object",
        "properties": {
          "lists": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "An existing list's id; none for a new one."
                },
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                },
                "aisles": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "maxItems": 40
                }
              },
              "required": [
                "name",
                "aisles"
              ]
            },
            "minItems": 1,
            "maxItems": 10
          }
        },
        "required": [
          "lists"
        ],
        "example": {
          "lists": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "Groceries",
              "aisles": []
            },
            {
              "name": "Costco",
              "aisles": [
                "0199e8b0-5a10-7a01-8b00-000000000021"
              ]
            }
          ]
        }
      },
      "PutAwayResult": {
        "type": "object",
        "properties": {
          "stored": {
            "type": "integer",
            "description": "Things now at home."
          },
          "cleared": {
            "type": "integer",
            "description": "Things cleared without going in: household things and what's always on hand."
          }
        },
        "required": [
          "stored",
          "cleared"
        ],
        "example": {
          "stored": 12,
          "cleared": 2
        }
      },
      "PutAway": {
        "type": "object",
        "properties": {
          "list": {
            "type": [
              "string",
              "null"
            ],
            "description": "A list's id: only what's checked off on it. Every list when left out."
          }
        },
        "example": {}
      },
      "ListItems": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItem"
            }
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "chicken thighs",
              "amount": "3 lb",
              "quantity": 3,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "checked": false,
              "source": "plan",
              "meals": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "name": "Lemon Chicken"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "ListRestore": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ListItemSnapshot"
            },
            "minItems": 1,
            "maxItems": 500
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "list": "0199e8b0-5a10-7a01-8b00-00000000001c",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 3,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "checked": false,
              "source": "plan",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 3,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "UsualBuys": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "food": {
                  "type": "string",
                  "description": "Which food it is."
                },
                "name": {
                  "type": "string"
                },
                "amount": {
                  "type": "string",
                  "description": "How much it was last time, or empty."
                },
                "every": {
                  "type": "integer",
                  "description": "About how many days apart it's bought."
                },
                "lastBought": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-15"
                },
                "dueOn": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-15"
                }
              },
              "required": [
                "food",
                "name",
                "amount",
                "every",
                "lastBought",
                "dueOn"
              ]
            },
            "description": "Due now, most overdue first."
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "food": "milk",
              "name": "milk",
              "amount": "1 gal",
              "every": 6,
              "lastBought": "2026-10-09",
              "dueOn": "2026-10-14"
            }
          ]
        }
      },
      "UsualAdd": {
        "type": "object",
        "properties": {
          "foods": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "minItems": 1,
            "maxItems": 50
          }
        },
        "required": [
          "foods"
        ],
        "example": {
          "foods": [
            "milk"
          ]
        }
      },
      "UsualSkip": {
        "type": "object",
        "properties": {
          "food": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          }
        },
        "required": [
          "food"
        ],
        "example": {
          "food": "milk"
        }
      },
      "Pantry": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PantryItem"
            },
            "description": "The fridge, the freezer, then the cupboard; by aisle in each."
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000026",
              "name": "chicken thighs",
              "amount": "2 lb",
              "quantity": 2,
              "unit": "pound",
              "amountText": null,
              "storedIn": "fridge",
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "useBy": "2026-10-18",
              "meals": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "name": "Lemon Chicken",
                  "date": "2026-10-17",
                  "status": "planned"
                }
              ],
              "useSoon": {
                "kind": "soon",
                "date": "2026-10-18"
              },
              "createdAt": "2026-10-15T18:30:00.000Z"
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000027",
              "name": "rice",
              "amount": "",
              "quantity": null,
              "unit": null,
              "amountText": null,
              "storedIn": "cupboard",
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000022",
              "useBy": null,
              "meals": [],
              "useSoon": null,
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "PantryItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "description": "How much, as it shows: \"2 lb\", \"half a bag\", or empty for just \"have it\"."
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ]
          },
          "amountText": {
            "type": [
              "string",
              "null"
            ]
          },
          "storedIn": {
            "$ref": "#/components/schemas/StoredIn"
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          },
          "useBy": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-20"
          },
          "meals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "date": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-20"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "planned",
                    "cooked",
                    "skipped"
                  ]
                }
              },
              "required": [
                "id",
                "name",
                "date",
                "status"
              ]
            },
            "description": "The planned meals it's set aside for."
          },
          "useSoon": {
            "$ref": "#/components/schemas/UseSoon"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "amount",
          "quantity",
          "unit",
          "amountText",
          "storedIn",
          "aisle",
          "useBy",
          "meals",
          "useSoon",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000026",
          "name": "chicken thighs",
          "amount": "2 lb",
          "quantity": 2,
          "unit": "pound",
          "amountText": null,
          "storedIn": "fridge",
          "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
          "useBy": "2026-10-18",
          "meals": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "name": "Lemon Chicken",
              "date": "2026-10-17",
              "status": "planned"
            }
          ],
          "useSoon": {
            "kind": "soon",
            "date": "2026-10-18"
          },
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "StoredIn": {
        "type": [
          "string",
          "null"
        ],
        "enum": [
          "fridge",
          "freezer",
          "cupboard",
          null
        ],
        "description": "Where it's kept."
      },
      "UseSoon": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "past"
                ]
              },
              "date": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                "example": "2026-10-20"
              }
            },
            "required": [
              "kind",
              "date"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "soon"
                ]
              },
              "date": {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                "example": "2026-10-20"
              }
            },
            "required": [
              "kind",
              "date"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "leftover"
                ]
              }
            },
            "required": [
              "kind"
            ]
          },
          {
            "type": "null"
          }
        ],
        "description": "Why to use it soon: its use-by date has gone (`past`) or is close (`soon`), or it's fresh food bought for meals that were skipped or went by uncooked (`leftover`)."
      },
      "PantryBefore": {
        "type": "object",
        "properties": {
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PantrySnapshot"
            }
          }
        },
        "required": [
          "before"
        ],
        "description": "What was there before, for Undo with /pantry/restore.",
        "example": {
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000026",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 2,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "storedIn": "fridge",
              "useBy": "2026-10-18",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 2,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "PantrySnapshot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "foodKey": {
            "type": "string",
            "maxLength": 200
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "amountText": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          },
          "storedIn": {
            "$ref": "#/components/schemas/StoredIn"
          },
          "useBy": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-20"
          },
          "forMeals": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "properties": {
                "meal": {
                  "type": "string"
                },
                "quantity": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "unit": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              },
              "required": [
                "meal",
                "quantity",
                "unit"
              ]
            },
            "maxItems": 100
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "foodKey",
          "quantity",
          "unit",
          "amountText",
          "aisle",
          "storedIn",
          "useBy",
          "forMeals",
          "createdAt"
        ],
        "description": "Something at home as it was, to put it back (Undo).",
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000026",
          "name": "chicken thighs",
          "foodKey": "chicken thigh",
          "quantity": 2,
          "unit": "pound",
          "amountText": null,
          "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
          "storedIn": "fridge",
          "useBy": "2026-10-18",
          "forMeals": [
            {
              "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "quantity": 2,
              "unit": "pound"
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "NewPantryItem": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "\"2 cans black beans\", or just \"rice\"."
          },
          "storedIn": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "fridge",
              "freezer",
              "cupboard",
              null
            ],
            "description": "Where it's kept; a guess from the food when left out."
          }
        },
        "required": [
          "text"
        ],
        "example": {
          "text": "2 cans black beans"
        }
      },
      "PantryBulkResult": {
        "type": "object",
        "properties": {
          "changed": {
            "type": "integer"
          },
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PantrySnapshot"
            }
          }
        },
        "required": [
          "changed",
          "before"
        ],
        "example": {
          "changed": 1,
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000026",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 2,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "storedIn": "fridge",
              "useBy": "2026-10-18",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 2,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "PantryBulk": {
        "type": "object",
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 500
          },
          "action": {
            "type": "string",
            "enum": [
              "remove",
              "storedIn",
              "useBy"
            ]
          },
          "storedIn": {
            "$ref": "#/components/schemas/StoredIn"
          },
          "useBy": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-20"
          }
        },
        "required": [
          "ids",
          "action"
        ],
        "example": {
          "ids": [
            "0199e8b0-5a10-7a01-8b00-000000000026"
          ],
          "action": "storedIn",
          "storedIn": "freezer"
        }
      },
      "PantryChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amount": {
            "type": "string",
            "maxLength": 60,
            "description": "\"2 lb\", \"half a bag\", or empty for just \"have it\"."
          },
          "storedIn": {
            "$ref": "#/components/schemas/StoredIn"
          },
          "useBy": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-20"
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "example": {
          "storedIn": "freezer"
        }
      },
      "ReceiptList": {
        "type": "object",
        "properties": {
          "receipts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Receipt"
            }
          }
        },
        "required": [
          "receipts"
        ],
        "example": {
          "receipts": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000002e",
              "store": "Walmart",
              "date": "2026-10-14",
              "total": 48.12,
              "lines": [
                {
                  "id": "0d1b5c2a9e44",
                  "text": "GV WHL MLK 1GAL",
                  "name": "whole milk",
                  "amount": "1 gal",
                  "price": 3.48,
                  "kind": "food",
                  "listItem": {
                    "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
                    "name": "milk"
                  }
                }
              ],
              "files": [
                {
                  "url": "/api/pantry/receipts/0199e8b0-5a10-7a01-8b00-00000000002e/files/0",
                  "mediaType": "image/jpeg"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "Receipt": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "store": {
            "type": [
              "string",
              "null"
            ]
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "description": "The day on it."
          },
          "total": {
            "type": [
              "number",
              "null"
            ]
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReceiptLine"
            }
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "mediaType": {
                  "type": "string",
                  "enum": [
                    "image/jpeg",
                    "application/pdf"
                  ]
                }
              },
              "required": [
                "url",
                "mediaType"
              ]
            },
            "description": "The receipt's photos or PDF, to compare with."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "store",
          "date",
          "total",
          "lines",
          "files",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002e",
          "store": "Walmart",
          "date": "2026-10-14",
          "total": 48.12,
          "lines": [
            {
              "id": "0d1b5c2a9e44",
              "text": "GV WHL MLK 1GAL",
              "name": "whole milk",
              "amount": "1 gal",
              "price": 3.48,
              "kind": "food",
              "listItem": {
                "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
                "name": "milk"
              }
            }
          ],
          "files": [
            {
              "url": "/api/pantry/receipts/0199e8b0-5a10-7a01-8b00-00000000002e/files/0",
              "mediaType": "image/jpeg"
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ReceiptLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "description": "As printed."
          },
          "name": {
            "type": "string",
            "description": "What it is, plainly."
          },
          "amount": {
            "type": [
              "string",
              "null"
            ]
          },
          "price": {
            "type": [
              "number",
              "null"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "food",
              "household",
              "other"
            ],
            "description": "Food goes in the pantry; household things and the rest don't, unless they're picked."
          },
          "listItem": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "name"
            ],
            "description": "The shopping list item it was bought for, taken off the list when it's added."
          }
        },
        "required": [
          "id",
          "text",
          "name",
          "amount",
          "price",
          "kind",
          "listItem"
        ],
        "example": {
          "id": "0d1b5c2a9e44",
          "text": "GV WHL MLK 1GAL",
          "name": "whole milk",
          "amount": "1 gal",
          "price": 3.48,
          "kind": "food",
          "listItem": {
            "id": "0199e8b0-5a10-7a01-8b00-00000000001d",
            "name": "milk"
          }
        }
      },
      "ReceiptAdded": {
        "type": "object",
        "properties": {
          "added": {
            "type": "integer"
          },
          "offTheList": {
            "type": "integer",
            "description": "Shopping list items taken off with them."
          }
        },
        "required": [
          "added",
          "offTheList"
        ],
        "example": {
          "added": 12,
          "offTheList": 4
        }
      },
      "AddReceiptInput": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                },
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 200
                },
                "amount": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 60
                }
              },
              "required": [
                "id",
                "name",
                "amount"
              ]
            },
            "minItems": 1,
            "maxItems": 300
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0d1b5c2a9e44",
              "name": "whole milk",
              "amount": "1 gal"
            }
          ]
        }
      },
      "PantryItems": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PantryItem"
            }
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000026",
              "name": "chicken thighs",
              "amount": "2 lb",
              "quantity": 2,
              "unit": "pound",
              "amountText": null,
              "storedIn": "fridge",
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "useBy": "2026-10-18",
              "meals": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "name": "Lemon Chicken",
                  "date": "2026-10-17",
                  "status": "planned"
                }
              ],
              "useSoon": {
                "kind": "soon",
                "date": "2026-10-18"
              },
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "PantryRestore": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PantrySnapshot"
            },
            "minItems": 1,
            "maxItems": 500
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000026",
              "name": "chicken thighs",
              "foodKey": "chicken thigh",
              "quantity": 2,
              "unit": "pound",
              "amountText": null,
              "aisle": "0199e8b0-5a10-7a01-8b00-000000000021",
              "storedIn": "fridge",
              "useBy": "2026-10-18",
              "forMeals": [
                {
                  "meal": "0199e8b0-5a10-7a01-8b00-00000000001b",
                  "quantity": 2,
                  "unit": "pound"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "Plan": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntry"
            }
          }
        },
        "required": [
          "entries"
        ],
        "example": {
          "entries": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "planned",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000006",
                "slug": "lemon-chicken",
                "name": "Lemon Chicken",
                "photo": null,
                "servings": 4,
                "totalMinutes": 50,
                "calories": 412
              },
              "withRecipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice",
                "calories": 210
              },
              "note": null,
              "servings": null,
              "why": [
                {
                  "kind": "theme",
                  "label": "Taco night"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "PlanEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15",
            "description": "Its day, or for the week's pool, the week's first day."
          },
          "anyDay": {
            "type": "boolean",
            "description": "In the week's pool: for any day of the week."
          },
          "meal": {
            "$ref": "#/components/schemas/Meal"
          },
          "position": {
            "type": "integer",
            "description": "Its place among the meal's dishes."
          },
          "status": {
            "$ref": "#/components/schemas/PlanStatus"
          },
          "recipe": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "photo": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "servings": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "totalMinutes": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "calories": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "In a serving, when its nutrition is known."
              }
            },
            "required": [
              "id",
              "slug",
              "name",
              "photo",
              "servings",
              "totalMinutes",
              "calories"
            ],
            "description": "Null for a note."
          },
          "withRecipe": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "calories": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "id",
              "slug",
              "name",
              "calories"
            ],
            "description": "What it's made with: \"with peanut sauce\"."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "A meal that isn't a recipe: \"leftovers\"."
          },
          "servings": {
            "type": [
              "number",
              "null"
            ],
            "description": "Null for the recipe's own servings."
          },
          "why": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Reason"
            },
            "maxItems": 5,
            "description": "Why it was picked, when Fill the week picked it: none otherwise."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "date",
          "anyDay",
          "meal",
          "position",
          "status",
          "recipe",
          "withRecipe",
          "note",
          "servings",
          "why",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
          "date": "2026-10-15",
          "anyDay": false,
          "meal": "dinner",
          "position": 0,
          "status": "planned",
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken",
            "photo": null,
            "servings": 4,
            "totalMinutes": 50,
            "calories": 412
          },
          "withRecipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000007",
            "slug": "garlic-rice",
            "name": "Garlic Rice",
            "calories": 210
          },
          "note": null,
          "servings": null,
          "why": [
            {
              "kind": "theme",
              "label": "Taco night"
            }
          ],
          "createdAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "Meal": {
        "type": "string",
        "enum": [
          "breakfast",
          "lunch",
          "dinner",
          "snack"
        ]
      },
      "PlanStatus": {
        "type": "string",
        "enum": [
          "planned",
          "cooked",
          "skipped"
        ]
      },
      "Reason": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "theme"
                ]
              },
              "label": {
                "type": "string",
                "maxLength": 60
              }
            },
            "required": [
              "kind",
              "label"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "themeMissed"
                ]
              },
              "label": {
                "type": "string",
                "maxLength": 60
              }
            },
            "required": [
              "kind",
              "label"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "usesUp"
                ]
              },
              "foods": {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 60
                },
                "maxItems": 5
              }
            },
            "required": [
              "kind",
              "foods"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "onSale"
                ]
              },
              "foods": {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 60
                },
                "maxItems": 5
              },
              "lowest": {
                "type": "boolean",
                "description": "One is the lowest it's been in a while."
              }
            },
            "required": [
              "kind",
              "foods",
              "lowest"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "new"
                ]
              }
            },
            "required": [
              "kind"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "notLately"
                ]
              },
              "weeks": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "kind",
              "weeks"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "rated"
                ]
              },
              "stars": {
                "type": "integer",
                "minimum": 1,
                "maximum": 5
              }
            },
            "required": [
              "kind",
              "stars"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "quick"
                ]
              },
              "minutes": {
                "type": "integer",
                "minimum": 0
              }
            },
            "required": [
              "kind",
              "minutes"
            ]
          },
          {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "leaveOut"
                ]
              },
              "foods": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "food": {
                      "type": "string",
                      "maxLength": 60
                    },
                    "who": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "food",
                    "who"
                  ]
                },
                "maxItems": 20
              }
            },
            "required": [
              "kind",
              "foods"
            ]
          }
        ],
        "description": "Why a meal was picked: the night's theme (\"Taco night\"), or one nothing fitted; food at home it uses up; food on sale at the household's store; never made; not had in so many weeks; rated so many stars; quick enough for a weeknight. And, after those, the foods to leave out of it for someone who doesn't like them.",
        "example": {
          "kind": "notLately",
          "weeks": 5
        }
      },
      "Autoplan": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "day": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "The day it runs, 0 for Sunday, in the household's time zone."
          },
          "hour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "The hour it runs, 0 to 23, in the household's time zone."
          },
          "meals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Meal"
            },
            "minItems": 1,
            "maxItems": 4,
            "description": "The meals it fills, of those the household plans."
          },
          "addToList": {
            "type": "boolean",
            "description": "Whether it puts what the week needs on the list too."
          },
          "next": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it runs next; none while it's off."
          },
          "last": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/AutoplanRun"
              },
              {
                "type": "null"
              }
            ],
            "description": "What it did last."
          }
        },
        "required": [
          "enabled",
          "day",
          "hour",
          "meals",
          "addToList",
          "next",
          "last"
        ],
        "example": {
          "enabled": true,
          "day": 6,
          "hour": 9,
          "meals": [
            "dinner"
          ],
          "addToList": true,
          "next": "2026-10-24T16:00:00.000Z",
          "last": {
            "week": "2026-10-19",
            "at": "2026-10-17T16:00:00.000Z",
            "planned": 6,
            "empty": 7,
            "listed": 18
          }
        }
      },
      "AutoplanRun": {
        "type": "object",
        "properties": {
          "week": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15",
            "description": "The first day of the week it planned."
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "planned": {
            "type": "integer",
            "description": "Meals it filled."
          },
          "empty": {
            "type": "integer",
            "description": "Meals that were empty to fill."
          },
          "listed": {
            "type": "integer",
            "description": "Lines it put on the list, new or added to."
          }
        },
        "required": [
          "week",
          "at",
          "planned",
          "empty",
          "listed"
        ],
        "example": {
          "week": "2026-10-19",
          "at": "2026-10-17T16:00:00.000Z",
          "planned": 6,
          "empty": 7,
          "listed": 18
        }
      },
      "AutoplanSettings": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "day": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "The day it runs, 0 for Sunday, in the household's time zone."
          },
          "hour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "The hour it runs, 0 to 23, in the household's time zone."
          },
          "meals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Meal"
            },
            "minItems": 1,
            "maxItems": 4,
            "description": "The meals it fills, of those the household plans."
          },
          "addToList": {
            "type": "boolean",
            "description": "Whether it puts what the week needs on the list too."
          }
        },
        "required": [
          "enabled",
          "day",
          "hour",
          "meals",
          "addToList"
        ],
        "example": {
          "enabled": true,
          "day": 6,
          "hour": 9,
          "meals": [
            "dinner"
          ],
          "addToList": true
        }
      },
      "WhatsNext": {
        "type": "object",
        "properties": {
          "today": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15",
            "description": "Today, in the household's time zone."
          },
          "mealNow": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Meal"
              },
              {
                "description": "The meal of the hour: breakfast until 11, lunch until 3."
              }
            ]
          },
          "entry": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlanEntry"
              },
              {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The next meal still planned, or null for none ahead."
              }
            ]
          }
        },
        "required": [
          "today",
          "mealNow",
          "entry"
        ],
        "example": {
          "today": "2026-10-15",
          "mealNow": "dinner",
          "entry": {
            "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
            "date": "2026-10-15",
            "anyDay": false,
            "meal": "dinner",
            "position": 0,
            "status": "planned",
            "recipe": {
              "id": "0199e8b0-5a10-7a01-8b00-000000000006",
              "slug": "lemon-chicken",
              "name": "Lemon Chicken",
              "photo": null,
              "servings": 4,
              "totalMinutes": 50,
              "calories": 412
            },
            "withRecipe": {
              "id": "0199e8b0-5a10-7a01-8b00-000000000007",
              "slug": "garlic-rice",
              "name": "Garlic Rice",
              "calories": 210
            },
            "note": null,
            "servings": null,
            "why": [
              {
                "kind": "theme",
                "label": "Taco night"
              }
            ],
            "createdAt": "2026-10-15T18:30:00.000Z"
          }
        }
      },
      "NewPlanEntry": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15"
          },
          "meal": {
            "$ref": "#/components/schemas/Meal"
          },
          "anyDay": {
            "type": "boolean",
            "description": "For the week's pool: any day of the week `date` is in."
          },
          "recipe": {
            "type": [
              "string",
              "null"
            ],
            "description": "The recipe, by id or address. Or give a note instead."
          },
          "withRecipe": {
            "type": [
              "string",
              "null"
            ],
            "description": "A side or sauce it's made with, by id or address."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "servings": {
            "type": [
              "number",
              "null"
            ],
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "why": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Reason"
            },
            "maxItems": 5,
            "description": "Why it was picked, as Fill the week said."
          }
        },
        "required": [
          "date",
          "meal"
        ],
        "example": {
          "date": "2026-10-15",
          "meal": "dinner",
          "recipe": "lemon-chicken",
          "withRecipe": "garlic-rice"
        }
      },
      "PlanEntries": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntry"
            }
          }
        },
        "required": [
          "entries"
        ],
        "example": {
          "entries": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "planned",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000006",
                "slug": "lemon-chicken",
                "name": "Lemon Chicken",
                "photo": null,
                "servings": 4,
                "totalMinutes": 50,
                "calories": 412
              },
              "withRecipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice",
                "calories": 210
              },
              "note": null,
              "servings": null,
              "why": [
                {
                  "kind": "theme",
                  "label": "Taco night"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ]
        }
      },
      "NewPlanEntries": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NewPlanEntry"
            },
            "minItems": 1,
            "maxItems": 56
          }
        },
        "required": [
          "entries"
        ],
        "example": {
          "entries": [
            {
              "date": "2026-10-15",
              "meal": "dinner",
              "recipe": "0199e8b0-5a10-7a01-8b00-000000000006",
              "withRecipe": "0199e8b0-5a10-7a01-8b00-000000000007",
              "why": [
                {
                  "kind": "notLately",
                  "weeks": 5
                }
              ]
            }
          ]
        }
      },
      "PlanBulkResult": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntry"
            },
            "description": "The meals as they are now; none when they were taken off."
          },
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntrySnapshot"
            },
            "description": "The meals as they were, for Undo."
          }
        },
        "required": [
          "entries",
          "before"
        ],
        "example": {
          "entries": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "cooked",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000006",
                "slug": "lemon-chicken",
                "name": "Lemon Chicken",
                "photo": null,
                "servings": 4,
                "totalMinutes": 50,
                "calories": 412
              },
              "withRecipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice",
                "calories": 210
              },
              "note": null,
              "servings": null,
              "why": [
                {
                  "kind": "theme",
                  "label": "Taco night"
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z"
            }
          ],
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "planned",
              "recipe": "0199e8b0-5a10-7a01-8b00-000000000006",
              "withRecipe": "0199e8b0-5a10-7a01-8b00-000000000007",
              "note": null,
              "servings": null,
              "listedAt": null,
              "why": null,
              "cookedAt": null
            }
          ]
        }
      },
      "PlanEntrySnapshot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15"
          },
          "anyDay": {
            "type": "boolean"
          },
          "meal": {
            "$ref": "#/components/schemas/Meal"
          },
          "position": {
            "type": "integer",
            "minimum": 0
          },
          "status": {
            "$ref": "#/components/schemas/PlanStatus"
          },
          "recipe": {
            "type": [
              "string",
              "null"
            ]
          },
          "withRecipe": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "servings": {
            "type": [
              "number",
              "null"
            ],
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "listedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "why": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Reason"
            },
            "maxItems": 5
          },
          "cookedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was marked cooked: put back cooked, its recipe and side were made then."
          }
        },
        "required": [
          "id",
          "date",
          "anyDay",
          "meal",
          "position",
          "status",
          "recipe",
          "withRecipe",
          "note",
          "servings",
          "listedAt"
        ],
        "description": "A meal as it was, to put back with /plan/restore: what taking it off or changing it answers with.",
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
          "date": "2026-10-15",
          "anyDay": false,
          "meal": "dinner",
          "position": 0,
          "status": "planned",
          "recipe": "0199e8b0-5a10-7a01-8b00-000000000006",
          "withRecipe": "0199e8b0-5a10-7a01-8b00-000000000007",
          "note": null,
          "servings": null,
          "listedAt": null,
          "why": null,
          "cookedAt": null
        }
      },
      "PlanBulk": {
        "type": "object",
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "minItems": 1,
            "maxItems": 200
          },
          "action": {
            "type": "string",
            "enum": [
              "remove",
              "move",
              "status"
            ]
          },
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15",
            "description": "Where to move them."
          },
          "meal": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Meal"
              },
              {
                "description": "The meal to move them to; their own if left out."
              }
            ]
          },
          "anyDay": {
            "type": "boolean",
            "description": "Into the pool of the week `date` is in."
          },
          "status": {
            "$ref": "#/components/schemas/PlanStatus"
          }
        },
        "required": [
          "ids",
          "action"
        ],
        "example": {
          "ids": [
            "0199e8b0-5a10-7a01-8b00-00000000001b"
          ],
          "action": "status",
          "status": "cooked"
        }
      },
      "PlanEntryChanges": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15"
          },
          "meal": {
            "$ref": "#/components/schemas/Meal"
          },
          "anyDay": {
            "type": "boolean"
          },
          "recipe": {
            "type": [
              "string",
              "null"
            ],
            "description": "Another recipe (Something else): it's planned afresh, its side kept."
          },
          "withRecipe": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "minLength": 1,
            "maxLength": 200
          },
          "servings": {
            "type": [
              "number",
              "null"
            ],
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "status": {
            "$ref": "#/components/schemas/PlanStatus"
          },
          "why": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Reason"
            },
            "maxItems": 5,
            "description": "Why a new recipe was picked, as Something else said."
          }
        },
        "example": {
          "date": "2026-10-16",
          "status": "planned"
        }
      },
      "PlanWeek": {
        "type": "object",
        "properties": {
          "week": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "example": "2026-10-15",
            "description": "The week's first day."
          }
        },
        "required": [
          "week"
        ],
        "example": {
          "week": "2026-10-11"
        }
      },
      "PlanCleared": {
        "type": "object",
        "properties": {
          "before": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntrySnapshot"
            },
            "description": "The meals taken off, for Undo."
          }
        },
        "required": [
          "before"
        ],
        "example": {
          "before": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "planned",
              "recipe": "0199e8b0-5a10-7a01-8b00-000000000006",
              "withRecipe": "0199e8b0-5a10-7a01-8b00-000000000007",
              "note": null,
              "servings": null,
              "listedAt": null,
              "why": null,
              "cookedAt": null
            }
          ]
        }
      },
      "PlanRestore": {
        "type": "object",
        "properties": {
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanEntrySnapshot"
            },
            "minItems": 1,
            "maxItems": 500
          }
        },
        "required": [
          "entries"
        ],
        "example": {
          "entries": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000001b",
              "date": "2026-10-15",
              "anyDay": false,
              "meal": "dinner",
              "position": 0,
              "status": "planned",
              "recipe": "0199e8b0-5a10-7a01-8b00-000000000006",
              "withRecipe": "0199e8b0-5a10-7a01-8b00-000000000007",
              "note": null,
              "servings": null,
              "listedAt": null,
              "why": null,
              "cookedAt": null
            }
          ]
        }
      },
      "Suggestions": {
        "type": "object",
        "properties": {
          "suggestions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-15"
                },
                "meal": {
                  "$ref": "#/components/schemas/Meal"
                },
                "recipe": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "photo": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "servings": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "totalMinutes": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "id",
                    "slug",
                    "name",
                    "photo",
                    "servings",
                    "totalMinutes"
                  ]
                },
                "withRecipe": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "id",
                    "slug",
                    "name"
                  ]
                },
                "why": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Reason"
                  },
                  "maxItems": 5
                }
              },
              "required": [
                "date",
                "meal",
                "recipe",
                "withRecipe",
                "why"
              ]
            }
          }
        },
        "required": [
          "suggestions"
        ],
        "example": {
          "suggestions": [
            {
              "date": "2026-10-15",
              "meal": "dinner",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000006",
                "slug": "lemon-chicken",
                "name": "Lemon Chicken",
                "photo": null,
                "servings": 4,
                "totalMinutes": 50
              },
              "withRecipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice"
              },
              "why": [
                {
                  "kind": "notLately",
                  "weeks": 5
                },
                {
                  "kind": "rated",
                  "stars": 5
                }
              ]
            }
          ]
        }
      },
      "SuggestRequest": {
        "type": "object",
        "properties": {
          "slots": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                  "example": "2026-10-15"
                },
                "meal": {
                  "$ref": "#/components/schemas/Meal"
                }
              },
              "required": [
                "date",
                "meal"
              ]
            },
            "minItems": 1,
            "maxItems": 56
          },
          "exclude": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 200,
            "description": "Recipes not to suggest: ideas already showing, or ones passed over."
          }
        },
        "required": [
          "slots"
        ],
        "example": {
          "slots": [
            {
              "date": "2026-10-15",
              "meal": "dinner"
            },
            {
              "date": "2026-10-16",
              "meal": "dinner"
            }
          ]
        }
      },
      "ProposalList": {
        "type": "object",
        "properties": {
          "proposals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Proposal"
            }
          },
          "total": {
            "type": "number",
            "description": "How many are waiting in all."
          }
        },
        "required": [
          "proposals",
          "total"
        ],
        "example": {
          "proposals": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000029",
              "kind": "tidy",
              "status": "pending",
              "resolvedAt": null,
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000006",
                "slug": "lemon-chicken",
                "name": "Lemon Chicken"
              },
              "summary": "Split a long step and marked the chicken as needed.",
              "sections": [
                "steps",
                "marks"
              ],
              "note": null,
              "payload": null,
              "chat": null,
              "before": {
                "steps": [
                  "Rub the chicken with lemon and garlic, then roast it at 200°C for 40 minutes until golden. Enjoy!"
                ],
                "marks": [
                  {
                    "line": "2 lb chicken thighs",
                    "mark": null
                  },
                  {
                    "line": "2 lemons, juiced",
                    "mark": null
                  }
                ]
              },
              "after": {
                "steps": [
                  "Rub the chicken with lemon and garlic.",
                  "Roast at 200°C for 40 minutes, until golden."
                ],
                "marks": [
                  {
                    "line": "2 lb chicken thighs",
                    "mark": "needed"
                  },
                  {
                    "line": "2 lemons, juiced",
                    "mark": "needed"
                  }
                ]
              },
              "createdAt": "2026-10-01T17:30:00.000Z"
            }
          ],
          "total": 1
        }
      },
      "Proposal": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "tidy",
              "note",
              "edit",
              "recipe",
              "meals",
              "list",
              "tastes",
              "import",
              "swap"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "applied",
              "dismissed"
            ]
          },
          "recipe": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "slug",
              "name"
            ]
          },
          "summary": {
            "type": "string",
            "description": "What it changes, in a sentence."
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProposalSection"
            },
            "description": "The sections it changes, each applied or skipped on its own."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "The cooking note it came from, for one made from a note."
          },
          "payload": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {},
            "description": "What an assistant's suggestion adds, by kind: `recipe` (a recipe, as the editor writes one), `meals` (meals for the plan), `items` (lines for the list), tastes to remember (`avoid`, `diets`, `weeknightMinutes`), a recipe page to import (`url`, `name`, `site`, `photo`, `minutes`, `why`), or a stand-in for an ingredient (`ingredientId`, `insteadOf`, `lines`, `note`, `steps`), which applying saves to the recipe. Null for changes to a recipe's sections."
          },
          "chat": {
            "type": [
              "string",
              "null"
            ],
            "description": "The assistant's chat it came from, for one from the assistant."
          },
          "before": {
            "$ref": "#/components/schemas/TidyValues"
          },
          "after": {
            "$ref": "#/components/schemas/TidyValues"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "resolvedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "kind",
          "status",
          "recipe",
          "summary",
          "sections",
          "note",
          "payload",
          "chat",
          "before",
          "after",
          "createdAt",
          "resolvedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000029",
          "kind": "tidy",
          "status": "pending",
          "resolvedAt": null,
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken"
          },
          "summary": "Split a long step and marked the chicken as needed.",
          "sections": [
            "steps",
            "marks"
          ],
          "note": null,
          "payload": null,
          "chat": null,
          "before": {
            "steps": [
              "Rub the chicken with lemon and garlic, then roast it at 200°C for 40 minutes until golden. Enjoy!"
            ],
            "marks": [
              {
                "line": "2 lb chicken thighs",
                "mark": null
              },
              {
                "line": "2 lemons, juiced",
                "mark": null
              }
            ]
          },
          "after": {
            "steps": [
              "Rub the chicken with lemon and garlic.",
              "Roast at 200°C for 40 minutes, until golden."
            ],
            "marks": [
              {
                "line": "2 lb chicken thighs",
                "mark": "needed"
              },
              {
                "line": "2 lemons, juiced",
                "mark": "needed"
              }
            ]
          },
          "createdAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "ProposalSection": {
        "type": "string",
        "enum": [
          "ingredients",
          "steps",
          "tags",
          "description",
          "details",
          "marks",
          "foods",
          "notes"
        ],
        "description": "A part of a recipe a suggestion can change: tidy up's sections, and the notes, which a cooking note can add to."
      },
      "TidyValues": {
        "type": "object",
        "properties": {
          "ingredients": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "details": {
            "type": "object",
            "properties": {
              "servings": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "prepMinutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "cookMinutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "totalMinutes": {
                "type": [
                  "number",
                  "null"
                ]
              }
            },
            "required": [
              "servings",
              "prepMinutes",
              "cookMinutes",
              "totalMinutes"
            ]
          },
          "marks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MarkedLine"
            }
          },
          "foods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FoodLine"
            }
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "description": "A recipe's sections, only the ones a suggestion changes. Ingredients and steps are editor lines."
      },
      "MarkedLine": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string"
          },
          "mark": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "needed",
              "swappable",
              "optional",
              null
            ]
          }
        },
        "required": [
          "line",
          "mark"
        ]
      },
      "FoodLine": {
        "type": "object",
        "properties": {
          "line": {
            "type": "string"
          },
          "food": {
            "type": [
              "string",
              "null"
            ],
            "description": "The food Merran knows that the line is, as a food key; null when no one has said."
          }
        },
        "required": [
          "line",
          "food"
        ]
      },
      "ApplyResult": {
        "type": "object",
        "properties": {
          "proposal": {
            "$ref": "#/components/schemas/Proposal"
          },
          "recipe": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Recipe"
              },
              {
                "type": "null"
              }
            ],
            "description": "The recipe it changed or made, if it did."
          }
        },
        "required": [
          "proposal",
          "recipe"
        ],
        "example": {
          "proposal": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000029",
            "kind": "tidy",
            "status": "applied",
            "resolvedAt": "2026-10-15T18:30:00.000Z",
            "recipe": {
              "id": "0199e8b0-5a10-7a01-8b00-000000000006",
              "slug": "lemon-chicken",
              "name": "Lemon Chicken"
            },
            "summary": "Split a long step and marked the chicken as needed.",
            "sections": [
              "steps",
              "marks"
            ],
            "note": null,
            "payload": null,
            "chat": null,
            "before": {
              "steps": [
                "Rub the chicken with lemon and garlic, then roast it at 200°C for 40 minutes until golden. Enjoy!"
              ],
              "marks": [
                {
                  "line": "2 lb chicken thighs",
                  "mark": null
                },
                {
                  "line": "2 lemons, juiced",
                  "mark": null
                }
              ]
            },
            "after": {
              "steps": [
                "Rub the chicken with lemon and garlic.",
                "Roast at 200°C for 40 minutes, until golden."
              ],
              "marks": [
                {
                  "line": "2 lb chicken thighs",
                  "mark": "needed"
                },
                {
                  "line": "2 lemons, juiced",
                  "mark": "needed"
                }
              ]
            },
            "createdAt": "2026-10-01T17:30:00.000Z"
          },
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken",
            "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
            "servings": 4,
            "totalMinutes": 50,
            "rating": 5,
            "tags": [
              "chicken",
              "dinner"
            ],
            "diets": [
              "gluten-free",
              "dairy-free",
              "egg-free",
              "nut-free"
            ],
            "photo": {
              "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
              "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
            },
            "lastMadeAt": "2026-10-09T01:15:00.000Z",
            "createdAt": "2026-10-01T17:30:00.000Z",
            "updatedAt": "2026-10-15T18:30:00.000Z",
            "origin": "link",
            "photoByAi": false,
            "sourceUrl": "https://example.com/lemon-chicken",
            "yieldText": null,
            "prepMinutes": 10,
            "cookMinutes": 40,
            "notes": "Bone-in thighs work too: give them 10 minutes more.",
            "dietOverrides": {},
            "needs": [],
            "nutrition": {
              "source": "calculated",
              "calories": 412,
              "protein": 31.5,
              "carbs": 6.2,
              "fat": 28.1,
              "fiber": 1.4,
              "sugar": 1.2,
              "sodium": 640,
              "missing": [],
              "servings": 4
            },
            "ingredients": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000011",
                "section": null,
                "text": "2 lb chicken thighs",
                "quantity": 2,
                "quantityMax": null,
                "unit": "pound",
                "food": "chicken thighs",
                "note": null,
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000012",
                "section": null,
                "text": "1 tbsp olive oil",
                "quantity": 1,
                "quantityMax": null,
                "unit": "tablespoon",
                "food": "olive oil",
                "note": null,
                "mark": "optional"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000013",
                "section": null,
                "text": "2 lemons, juiced",
                "quantity": 2,
                "quantityMax": null,
                "unit": null,
                "food": "lemons",
                "note": "juiced",
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000014",
                "section": null,
                "text": "4 cloves garlic, smashed",
                "quantity": 4,
                "quantityMax": null,
                "unit": "clove",
                "food": "garlic",
                "note": "smashed",
                "mark": "swappable"
              }
            ],
            "steps": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000015",
                "section": null,
                "text": "Heat the oven to 425°F."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000016",
                "section": null,
                "text": "Toss the chicken with the oil, lemon juice, garlic, salt and pepper."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000017",
                "section": null,
                "text": "Roast skin side up for 40 minutes, until crisp."
              }
            ]
          }
        }
      },
      "Recipe": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecipeSummary"
          },
          {
            "type": "object",
            "properties": {
              "origin": {
                "type": "string",
                "enum": [
                  "link",
                  "video",
                  "text",
                  "photo",
                  "manual",
                  "assistant",
                  "mealie",
                  "paprika",
                  "merran"
                ]
              },
              "photoByAi": {
                "type": "boolean",
                "description": "Its photo was made by AI, in the household's style. Making another replaces it; a real one always does."
              },
              "sourceUrl": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "yieldText": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prepMinutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "cookMinutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "notes": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dietOverrides": {
                "$ref": "#/components/schemas/DietOverrides"
              },
              "needs": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Foods the household said it can't do without: a dislike that's usually left out of recipes rules this one out instead."
              },
              "nutrition": {
                "$ref": "#/components/schemas/Nutrition"
              },
              "ingredients": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Ingredient"
                }
              },
              "steps": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Step"
                }
              }
            },
            "required": [
              "origin",
              "photoByAi",
              "sourceUrl",
              "yieldText",
              "prepMinutes",
              "cookMinutes",
              "notes",
              "dietOverrides",
              "needs",
              "nutrition",
              "ingredients",
              "steps"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000006",
          "slug": "lemon-chicken",
          "name": "Lemon Chicken",
          "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
          "servings": 4,
          "totalMinutes": 50,
          "rating": 5,
          "tags": [
            "chicken",
            "dinner"
          ],
          "diets": [
            "gluten-free",
            "dairy-free",
            "egg-free",
            "nut-free"
          ],
          "photo": {
            "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
            "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
          },
          "lastMadeAt": "2026-10-09T01:15:00.000Z",
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-15T18:30:00.000Z",
          "origin": "link",
          "photoByAi": false,
          "sourceUrl": "https://example.com/lemon-chicken",
          "yieldText": null,
          "prepMinutes": 10,
          "cookMinutes": 40,
          "notes": "Bone-in thighs work too: give them 10 minutes more.",
          "dietOverrides": {},
          "needs": [],
          "nutrition": {
            "source": "calculated",
            "calories": 412,
            "protein": 31.5,
            "carbs": 6.2,
            "fat": 28.1,
            "fiber": 1.4,
            "sugar": 1.2,
            "sodium": 640,
            "missing": [],
            "servings": 4
          },
          "ingredients": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000011",
              "section": null,
              "text": "2 lb chicken thighs",
              "quantity": 2,
              "quantityMax": null,
              "unit": "pound",
              "food": "chicken thighs",
              "note": null,
              "mark": "needed"
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000012",
              "section": null,
              "text": "1 tbsp olive oil",
              "quantity": 1,
              "quantityMax": null,
              "unit": "tablespoon",
              "food": "olive oil",
              "note": null,
              "mark": "optional"
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000013",
              "section": null,
              "text": "2 lemons, juiced",
              "quantity": 2,
              "quantityMax": null,
              "unit": null,
              "food": "lemons",
              "note": "juiced",
              "mark": "needed"
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000014",
              "section": null,
              "text": "4 cloves garlic, smashed",
              "quantity": 4,
              "quantityMax": null,
              "unit": "clove",
              "food": "garlic",
              "note": "smashed",
              "mark": "swappable"
            }
          ],
          "steps": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000015",
              "section": null,
              "text": "Heat the oven to 425°F."
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000016",
              "section": null,
              "text": "Toss the chicken with the oil, lemon juice, garlic, salt and pepper."
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000017",
              "section": null,
              "text": "Roast skin side up for 40 minutes, until crisp."
            }
          ]
        }
      },
      "Diet": {
        "type": "string",
        "enum": [
          "vegetarian",
          "vegan",
          "gluten-free",
          "dairy-free",
          "egg-free",
          "nut-free"
        ]
      },
      "DietOverrides": {
        "type": "object",
        "properties": {
          "vegetarian": {
            "type": "boolean"
          },
          "vegan": {
            "type": "boolean"
          },
          "gluten-free": {
            "type": "boolean"
          },
          "dairy-free": {
            "type": "boolean"
          },
          "egg-free": {
            "type": "boolean"
          },
          "nut-free": {
            "type": "boolean"
          }
        },
        "description": "Where the household corrected the labels the ingredients suggest."
      },
      "Nutrition": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "site",
              "calculated",
              "estimate"
            ],
            "description": "From the recipe's site, worked out from USDA's data, or estimated by AI."
          },
          "calories": {
            "type": "number",
            "description": "kcal."
          },
          "protein": {
            "type": "number",
            "description": "Grams."
          },
          "carbs": {
            "type": "number",
            "description": "Grams."
          },
          "fat": {
            "type": "number",
            "description": "Grams."
          },
          "fiber": {
            "type": "number",
            "description": "Grams."
          },
          "sugar": {
            "type": "number",
            "description": "Grams."
          },
          "sodium": {
            "type": "number",
            "description": "Milligrams."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ingredient lines that couldn't be counted."
          },
          "servings": {
            "type": "number",
            "description": "The servings it's for; it rescales when they change."
          }
        },
        "required": [
          "source"
        ],
        "description": "Per serving, when it's known.",
        "example": {
          "source": "calculated",
          "calories": 412,
          "protein": 31.5,
          "carbs": 12.2,
          "fat": 26.1,
          "fiber": 2.4,
          "sugar": 3.1,
          "sodium": 640,
          "missing": [
            "1 bay leaf"
          ],
          "servings": 4
        }
      },
      "Ingredient": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "section": {
            "type": [
              "string",
              "null"
            ]
          },
          "text": {
            "type": "string",
            "description": "The line as written."
          },
          "quantity": {
            "type": [
              "number",
              "null"
            ]
          },
          "quantityMax": {
            "type": [
              "number",
              "null"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "example": "tablespoon"
          },
          "food": {
            "type": [
              "string",
              "null"
            ]
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          },
          "mark": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "needed",
              "swappable",
              "optional",
              null
            ],
            "description": "What it is to the dish, as tidy up marked it: needed, swappable or optional. A dislike is left out of lines that aren't needed. None until marked."
          }
        },
        "required": [
          "id",
          "section",
          "text",
          "quantity",
          "quantityMax",
          "unit",
          "food",
          "note",
          "mark"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000012",
          "section": null,
          "text": "1 tbsp olive oil",
          "quantity": 1,
          "quantityMax": null,
          "unit": "tablespoon",
          "food": "olive oil",
          "note": null,
          "mark": "optional"
        }
      },
      "Step": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "section": {
            "type": [
              "string",
              "null"
            ]
          },
          "text": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "section",
          "text"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000015",
          "section": null,
          "text": "Heat the oven to 425°F."
        }
      },
      "RecipeSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "example": "lemon-chicken"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "servings": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalMinutes": {
            "type": [
              "number",
              "null"
            ],
            "description": "As given, or prep and cook added up."
          },
          "rating": {
            "type": [
              "number",
              "null"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "diets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Diet"
            },
            "description": "Diet labels worked out from the ingredients, with the household's corrections."
          },
          "photo": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "full": {
                "type": "string",
                "description": "For the recipe page."
              },
              "card": {
                "type": "string",
                "description": "Cropped, for recipe cards."
              }
            },
            "required": [
              "full",
              "card"
            ],
            "description": "Where its photo is, in each size, or null. The links change with the photo, so they can be kept."
          },
          "lastMadeAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "description",
          "servings",
          "totalMinutes",
          "rating",
          "tags",
          "diets",
          "photo",
          "lastMadeAt",
          "createdAt",
          "updatedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000006",
          "slug": "lemon-chicken",
          "name": "Lemon Chicken",
          "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
          "servings": 4,
          "totalMinutes": 50,
          "rating": 5,
          "tags": [
            "chicken",
            "dinner"
          ],
          "diets": [
            "gluten-free",
            "dairy-free",
            "egg-free",
            "nut-free"
          ],
          "photo": {
            "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
            "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
          },
          "lastMadeAt": "2026-10-09T01:15:00.000Z",
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-15T18:30:00.000Z"
        }
      },
      "ApplyInput": {
        "type": "object",
        "properties": {
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProposalSection"
            },
            "description": "For changes to a recipe: the sections to apply. The rest are skipped."
          }
        },
        "example": {
          "sections": [
            "steps",
            "marks"
          ]
        }
      },
      "RecipeList": {
        "type": "object",
        "properties": {
          "recipes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RecipeSummary"
            }
          },
          "total": {
            "type": "number",
            "description": "How many match, in all."
          }
        },
        "required": [
          "recipes",
          "total"
        ],
        "example": {
          "recipes": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000006",
              "slug": "lemon-chicken",
              "name": "Lemon Chicken",
              "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
              "servings": 4,
              "totalMinutes": 50,
              "rating": 5,
              "tags": [
                "chicken",
                "dinner"
              ],
              "diets": [
                "gluten-free",
                "dairy-free",
                "egg-free",
                "nut-free"
              ],
              "photo": {
                "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
                "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
              },
              "lastMadeAt": "2026-10-09T01:15:00.000Z",
              "createdAt": "2026-10-01T17:30:00.000Z",
              "updatedAt": "2026-10-15T18:30:00.000Z"
            }
          ],
          "total": 1
        }
      },
      "RecipeInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "servings": {
            "type": [
              "number",
              "null"
            ],
            "exclusiveMinimum": 0,
            "maximum": 1000
          },
          "yieldText": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 100,
            "description": "What it makes, when that isn't servings: \"2 loaves\"."
          },
          "prepMinutes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "cookMinutes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "totalMinutes": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000
          },
          "sourceUrl": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "string",
                "enum": [
                  ""
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "rating": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 5
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "maxItems": 30,
            "default": []
          },
          "ingredients": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 500
            },
            "maxItems": 300,
            "default": [],
            "example": [
              "# Dough",
              "2 cups flour",
              "1 tsp salt"
            ]
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 5000
            },
            "maxItems": 200,
            "default": []
          }
        },
        "required": [
          "name"
        ],
        "example": {
          "name": "Lemon Chicken",
          "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
          "servings": 4,
          "prepMinutes": 10,
          "cookMinutes": 40,
          "sourceUrl": "https://example.com/lemon-chicken",
          "tags": [
            "chicken",
            "dinner"
          ],
          "ingredients": [
            "2 lb chicken thighs",
            "1 tbsp olive oil",
            "2 lemons, juiced",
            "4 cloves garlic, smashed"
          ],
          "steps": [
            "Heat the oven to 425°F.",
            "Toss the chicken with the oil, lemon juice, garlic, salt and pepper.",
            "Roast skin side up for 40 minutes, until crisp."
          ]
        }
      },
      "BulkResult": {
        "type": "object",
        "properties": {
          "changed": {
            "type": "number",
            "description": "How many it changed. Ones not there, or already that way, are skipped."
          }
        },
        "required": [
          "changed"
        ],
        "example": {
          "changed": 2
        }
      },
      "RecipeBulkInput": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "delete"
                ]
              },
              "ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "minItems": 1,
                "maxItems": 500
              }
            },
            "required": [
              "action",
              "ids"
            ],
            "description": "To Recently deleted, for 30 days."
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "addTags"
                ]
              },
              "ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "minItems": 1,
                "maxItems": 500
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                },
                "minItems": 1,
                "maxItems": 20
              }
            },
            "required": [
              "action",
              "ids",
              "tags"
            ]
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "removeTags"
                ]
              },
              "ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "minItems": 1,
                "maxItems": 500
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                },
                "minItems": 1,
                "maxItems": 20
              }
            },
            "required": [
              "action",
              "ids",
              "tags"
            ]
          }
        ],
        "example": {
          "action": "addTags",
          "ids": [
            "0199e8b0-5a10-7a01-8b00-000000000006",
            "0199e8b0-5a10-7a01-8b00-000000000007"
          ],
          "tags": [
            "weeknight"
          ]
        }
      },
      "DeletedRecipe": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RecipeSummary"
          },
          {
            "type": "object",
            "properties": {
              "deletedAt": {
                "type": "string",
                "format": "date-time"
              },
              "goneAt": {
                "type": "string",
                "format": "date-time",
                "description": "When it's deleted for good."
              }
            },
            "required": [
              "deletedAt",
              "goneAt"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000006",
          "slug": "lemon-chicken",
          "name": "Lemon Chicken",
          "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
          "servings": 4,
          "totalMinutes": 50,
          "rating": 5,
          "tags": [
            "chicken",
            "dinner"
          ],
          "diets": [
            "gluten-free",
            "dairy-free",
            "egg-free",
            "nut-free"
          ],
          "photo": {
            "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
            "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
          },
          "lastMadeAt": "2026-10-09T01:15:00.000Z",
          "createdAt": "2026-10-01T17:30:00.000Z",
          "updatedAt": "2026-10-15T18:30:00.000Z",
          "deletedAt": "2026-10-15T18:30:00.000Z",
          "goneAt": "2026-11-14T18:30:00.000Z"
        }
      },
      "DeletedBulkInput": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "restore"
                ]
              },
              "ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "minItems": 1,
                "maxItems": 500
              }
            },
            "required": [
              "action",
              "ids"
            ],
            "description": "Back where they were."
          },
          {
            "type": "object",
            "properties": {
              "action": {
                "type": "string",
                "enum": [
                  "purge"
                ]
              },
              "ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "minItems": 1,
                "maxItems": 500
              }
            },
            "required": [
              "action",
              "ids"
            ],
            "description": "Gone for good, photos and all."
          }
        ],
        "example": {
          "action": "restore",
          "ids": [
            "0199e8b0-5a10-7a01-8b00-000000000006"
          ]
        }
      },
      "ImportResult": {
        "type": "object",
        "properties": {
          "recipe": {
            "$ref": "#/components/schemas/Recipe"
          },
          "duplicate": {
            "type": "boolean",
            "description": "True when the link was imported before: this is that recipe, unchanged."
          }
        },
        "required": [
          "recipe",
          "duplicate"
        ],
        "example": {
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken",
            "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
            "servings": 4,
            "totalMinutes": 50,
            "rating": 5,
            "tags": [
              "chicken",
              "dinner"
            ],
            "diets": [
              "gluten-free",
              "dairy-free",
              "egg-free",
              "nut-free"
            ],
            "photo": {
              "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
              "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
            },
            "lastMadeAt": "2026-10-09T01:15:00.000Z",
            "createdAt": "2026-10-01T17:30:00.000Z",
            "updatedAt": "2026-10-15T18:30:00.000Z",
            "origin": "link",
            "photoByAi": false,
            "sourceUrl": "https://example.com/lemon-chicken",
            "yieldText": null,
            "prepMinutes": 10,
            "cookMinutes": 40,
            "notes": "Bone-in thighs work too: give them 10 minutes more.",
            "dietOverrides": {},
            "needs": [],
            "nutrition": {
              "source": "calculated",
              "calories": 412,
              "protein": 31.5,
              "carbs": 6.2,
              "fat": 28.1,
              "fiber": 1.4,
              "sugar": 1.2,
              "sodium": 640,
              "missing": [],
              "servings": 4
            },
            "ingredients": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000011",
                "section": null,
                "text": "2 lb chicken thighs",
                "quantity": 2,
                "quantityMax": null,
                "unit": "pound",
                "food": "chicken thighs",
                "note": null,
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000012",
                "section": null,
                "text": "1 tbsp olive oil",
                "quantity": 1,
                "quantityMax": null,
                "unit": "tablespoon",
                "food": "olive oil",
                "note": null,
                "mark": "optional"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000013",
                "section": null,
                "text": "2 lemons, juiced",
                "quantity": 2,
                "quantityMax": null,
                "unit": null,
                "food": "lemons",
                "note": "juiced",
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000014",
                "section": null,
                "text": "4 cloves garlic, smashed",
                "quantity": 4,
                "quantityMax": null,
                "unit": "clove",
                "food": "garlic",
                "note": "smashed",
                "mark": "swappable"
              }
            ],
            "steps": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000015",
                "section": null,
                "text": "Heat the oven to 425°F."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000016",
                "section": null,
                "text": "Toss the chicken with the oil, lemon juice, garlic, salt and pepper."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000017",
                "section": null,
                "text": "Roast skin side up for 40 minutes, until crisp."
              }
            ]
          },
          "duplicate": false
        }
      },
      "LinkImportList": {
        "type": "object",
        "properties": {
          "imports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkImport"
            }
          }
        },
        "required": [
          "imports"
        ],
        "example": {
          "imports": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
              "status": "running",
              "tag": "to try",
              "total": 2,
              "done": 1,
              "added": 1,
              "already": 0,
              "failed": 0,
              "stopped": 0,
              "items": [
                {
                  "url": "https://example.com/garlic-rice",
                  "state": "added",
                  "recipe": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                    "slug": "garlic-rice",
                    "name": "Garlic Rice",
                    "photo": null
                  },
                  "error": null
                },
                {
                  "url": "https://example.com/lemon-chicken",
                  "state": "adding",
                  "recipe": null,
                  "error": null
                }
              ],
              "createdAt": "2026-10-15T18:30:00.000Z",
              "finishedAt": null
            }
          ]
        }
      },
      "LinkImportRequest": {
        "type": "object",
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 2000
            },
            "minItems": 1,
            "maxItems": 100
          },
          "tag": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40,
            "description": "Added to every recipe it makes, like \"to try\"."
          }
        },
        "required": [
          "urls"
        ],
        "example": {
          "urls": [
            "https://example.com/garlic-rice",
            "https://example.com/lemon-chicken"
          ],
          "tag": "to try"
        }
      },
      "LinkCheck": {
        "type": "object",
        "properties": {
          "links": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "video": {
                  "type": "boolean",
                  "description": "A video link, which can't be imported yet."
                },
                "existing": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "slug",
                    "name"
                  ],
                  "description": "In the library already: it's skipped."
                }
              },
              "required": [
                "url",
                "video",
                "existing"
              ]
            }
          },
          "more": {
            "type": "integer",
            "description": "Links past the first 100, which are left out."
          }
        },
        "required": [
          "links",
          "more"
        ],
        "example": {
          "links": [
            {
              "url": "https://example.com/lemon-chicken",
              "video": false,
              "existing": {
                "slug": "lemon-chicken",
                "name": "Lemon Chicken"
              }
            },
            {
              "url": "https://example.com/garlic-rice",
              "video": false,
              "existing": null
            }
          ],
          "more": 0
        }
      },
      "LinkCheckRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 100000,
            "description": "Pasted text with links in it: one per line, or mixed in."
          }
        },
        "required": [
          "text"
        ],
        "example": {
          "text": "Dinners to try:\nhttps://example.com/lemon-chicken\nhttps://example.com/garlic-rice"
        }
      },
      "ReadPage": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "title": {
            "type": "string",
            "maxLength": 500,
            "default": ""
          },
          "jsonld": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 400000
            },
            "maxItems": 20,
            "default": [],
            "description": "The page's JSON-LD blocks, as found."
          },
          "images": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 2000
            },
            "maxItems": 10,
            "default": [],
            "description": "Its share images: og:image and the like."
          },
          "text": {
            "type": "string",
            "maxLength": 100000,
            "default": "",
            "description": "Its text, as the browser shows it."
          }
        },
        "required": [
          "url"
        ],
        "example": {
          "url": "https://cooking.example/lemon-chicken",
          "title": "Lemon Chicken | Cooking",
          "jsonld": [
            "{\"@type\":\"Recipe\",\"name\":\"Lemon Chicken\"}"
          ],
          "images": [
            "https://cooking.example/lemon-chicken.jpg"
          ],
          "text": "Lemon Chicken. Serves 4. 4 chicken thighs, 2 lemons..."
        }
      },
      "TextImport": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 50000
          },
          "sourceUrl": {
            "anyOf": [
              {
                "type": "string",
                "format": "uri"
              },
              {
                "type": "string",
                "enum": [
                  ""
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Where it's from, when that's a page."
          }
        },
        "required": [
          "text"
        ],
        "example": {
          "text": "Lemon Chicken\n\n4 chicken thighs\n2 lemons, juiced\n1 tsp salt\n\nRub the chicken with lemon and salt. Roast at 200°C for 40 minutes.",
          "sourceUrl": null
        }
      },
      "NutritionLibrary": {
        "type": "object",
        "properties": {
          "missing": {
            "type": "integer",
            "description": "Recipes with servings and ingredients but no nutrition."
          },
          "noServings": {
            "type": "integer",
            "description": "Recipes without nutrition that need servings first."
          },
          "run": {
            "$ref": "#/components/schemas/NutritionRun"
          }
        },
        "required": [
          "missing",
          "noServings",
          "run"
        ],
        "example": {
          "missing": 42,
          "noServings": 3,
          "run": {
            "id": "0199e8b0-5a10-7a01-8b00-00000000002f",
            "status": "running",
            "total": 42,
            "done": 12,
            "workedOut": 11,
            "failed": 1,
            "stopped": null,
            "startedAt": "2026-10-15T18:30:00.000Z",
            "finishedAt": null
          }
        }
      },
      "NutritionRun": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "workedOut": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "stopped": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it stopped before the end, when it did."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "total",
          "done",
          "workedOut",
          "failed",
          "stopped",
          "startedAt",
          "finishedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002f",
          "status": "running",
          "total": 42,
          "done": 12,
          "workedOut": 11,
          "failed": 1,
          "stopped": null,
          "startedAt": "2026-10-15T18:30:00.000Z",
          "finishedAt": null
        },
        "description": "The latest run, going or finished in the last day."
      },
      "PhotoLibrary": {
        "type": "object",
        "properties": {
          "missing": {
            "type": "integer",
            "description": "Recipes without a photo."
          },
          "ready": {
            "type": "boolean",
            "description": "Whether AI photos can be made for the household now."
          },
          "why": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "off",
              "noKey",
              null
            ],
            "description": "Why not: turned off on the server, or no OpenAI or Google key for the household."
          },
          "style": {
            "type": "string",
            "enum": [
              "marble",
              "rustic",
              "bright",
              "moody"
            ],
            "description": "How the household's photos look; change it at `PATCH /settings/household` (`photos.style`)."
          },
          "run": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PhotoRun"
              },
              {
                "type": "null"
              }
            ],
            "description": "The latest run, going or finished in the last day."
          }
        },
        "required": [
          "missing",
          "ready",
          "why",
          "style",
          "run"
        ],
        "example": {
          "missing": 30,
          "ready": true,
          "why": null,
          "style": "marble",
          "run": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000033",
            "status": "running",
            "total": 30,
            "done": 8,
            "made": 8,
            "failed": 0,
            "stopped": null,
            "startedAt": "2026-10-15T18:30:00.000Z",
            "finishedAt": null
          }
        }
      },
      "PhotoRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "total": {
            "type": "integer"
          },
          "done": {
            "type": "integer"
          },
          "made": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "stopped": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it stopped before the end, when it did."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "total",
          "done",
          "made",
          "failed",
          "stopped",
          "startedAt",
          "finishedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000033",
          "status": "running",
          "total": 30,
          "done": 8,
          "made": 8,
          "failed": 0,
          "stopped": null,
          "startedAt": "2026-10-15T18:30:00.000Z",
          "finishedAt": null
        }
      },
      "Tag": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "recipes": {
            "type": "number",
            "description": "How many recipes have it."
          }
        },
        "required": [
          "id",
          "name",
          "recipes"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000008",
          "name": "weeknight",
          "recipes": 12
        }
      },
      "TagChanges": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "A name another tag already has merges the two."
          }
        },
        "required": [
          "name"
        ],
        "example": {
          "name": "Weeknight"
        }
      },
      "TidyRuns": {
        "type": "object",
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TidyRun"
            }
          },
          "untidied": {
            "type": "number",
            "description": "How many of its recipes tidy up hasn't looked at yet."
          }
        },
        "required": [
          "runs",
          "untidied"
        ],
        "example": {
          "runs": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000002a",
              "status": "running",
              "library": true,
              "total": 52,
              "done": 12,
              "suggested": 9,
              "fine": 3,
              "failed": 0,
              "stopped": null,
              "startedAt": "2026-10-01T17:30:00.000Z",
              "finishedAt": null
            }
          ],
          "untidied": 40
        }
      },
      "TidyRun": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "done",
              "failed",
              "cancelled"
            ]
          },
          "library": {
            "type": "boolean",
            "description": "Every recipe not tidied yet, rather than ones picked."
          },
          "total": {
            "type": "number"
          },
          "done": {
            "type": "number"
          },
          "suggested": {
            "type": "number",
            "description": "Recipes with suggestions waiting in Review."
          },
          "fine": {
            "type": "number",
            "description": "Recipes with nothing to change."
          },
          "failed": {
            "type": "number"
          },
          "stopped": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it stopped before the end, when it did: no AI, this month's limit, no credit, or three failures in a row."
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "library",
          "total",
          "done",
          "suggested",
          "fine",
          "failed",
          "stopped",
          "startedAt",
          "finishedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000002a",
          "status": "running",
          "library": true,
          "total": 52,
          "done": 12,
          "suggested": 9,
          "fine": 3,
          "failed": 0,
          "stopped": null,
          "startedAt": "2026-10-01T17:30:00.000Z",
          "finishedAt": null
        }
      },
      "TidyRunInput": {
        "type": "object",
        "properties": {
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "maxItems": 200,
            "description": "The recipes to tidy. Left out, every recipe not tidied yet, which only household admins can start."
          }
        },
        "example": {
          "ids": [
            "0199e8b0-5a10-7a01-8b00-000000000006",
            "0199e8b0-5a10-7a01-8b00-000000000007"
          ]
        }
      },
      "CookNote": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "rating": {
            "type": [
              "number",
              "null"
            ],
            "description": "1 to 5 stars."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "description": "For next time."
          },
          "by": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who cooked it, if they're still here."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "rating",
          "note",
          "by",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000c",
          "rating": 5,
          "note": "More lemon next time.",
          "by": "Sam Rivera",
          "createdAt": "2026-10-09T01:15:00.000Z"
        }
      },
      "AddedCookNote": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CookNote"
          },
          {
            "type": "object",
            "properties": {
              "suggesting": {
                "type": "boolean",
                "description": "Its note is being turned into a suggested change to the recipe, which will wait in the Review queue."
              }
            },
            "required": [
              "suggesting"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000c",
          "rating": 5,
          "note": "More lemon next time.",
          "by": "Sam Rivera",
          "createdAt": "2026-10-09T01:15:00.000Z",
          "suggesting": true
        }
      },
      "CookNoteInput": {
        "type": "object",
        "properties": {
          "rating": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 5,
            "description": "1 to 5 stars. They become the recipe's rating."
          },
          "note": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000,
            "description": "Anything to change next time."
          }
        },
        "example": {
          "rating": 5,
          "note": "More lemon next time."
        }
      },
      "DietsInput": {
        "type": "object",
        "properties": {
          "overrides": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DietOverrides"
              },
              {
                "description": "The labels as they should be, where they differ from the ingredients. An empty object goes back to what the ingredients say."
              }
            ]
          }
        },
        "required": [
          "overrides"
        ],
        "description": "For example, not gluten-free after all, because the household's stock has wheat in it.",
        "example": {
          "overrides": {
            "gluten-free": false
          }
        }
      },
      "NeedsInput": {
        "type": "object",
        "properties": {
          "foods": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 60
            },
            "maxItems": 50,
            "description": "Foods it can't do without, replacing any said before. An empty list lets every dislike be left out again."
          },
          "canLeaveOut": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 60
            },
            "maxItems": 50,
            "description": "Foods tidy up marked as needed that it can do without after all: their lines are marked optional."
          }
        },
        "required": [
          "foods"
        ],
        "description": "For example, a stir fry that's nothing without its mushrooms: someone who doesn't like them can't just leave them out.",
        "example": {
          "foods": [
            "mushrooms"
          ]
        }
      },
      "RecipePairings": {
        "type": "object",
        "properties": {
          "pairings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pairing"
            },
            "description": "What it goes with, as set."
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PairOption"
            },
            "description": "Every recipe that adds up to: picked ones first, then tagged ones by name."
          },
          "pairsWith": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PairOption"
            },
            "description": "Recipes it's offered with: the other way round."
          }
        },
        "required": [
          "pairings",
          "options",
          "pairsWith"
        ],
        "example": {
          "pairings": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000000e",
              "kind": "recipe",
              "recipe": {
                "id": "0199e8b0-5a10-7a01-8b00-000000000007",
                "slug": "garlic-rice",
                "name": "Garlic Rice",
                "photo": null,
                "servings": 4,
                "totalMinutes": 25
              }
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000000f",
              "kind": "tag",
              "tag": "salad"
            }
          ],
          "options": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000007",
              "slug": "garlic-rice",
              "name": "Garlic Rice",
              "photo": null,
              "servings": 4,
              "totalMinutes": 25
            }
          ],
          "pairsWith": []
        }
      },
      "Pairing": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "recipe"
                ]
              },
              "recipe": {
                "$ref": "#/components/schemas/PairOption"
              }
            },
            "required": [
              "id",
              "kind",
              "recipe"
            ]
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "tag"
                ]
              },
              "tag": {
                "type": "string",
                "description": "Every recipe with this tag."
              }
            },
            "required": [
              "id",
              "kind",
              "tag"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000e",
          "kind": "recipe",
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000007",
            "slug": "garlic-rice",
            "name": "Garlic Rice",
            "photo": null,
            "servings": 4,
            "totalMinutes": 25
          }
        }
      },
      "PairOption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "photo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Its card-sized photo, or null."
          },
          "servings": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalMinutes": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "slug",
          "name",
          "photo",
          "servings",
          "totalMinutes"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000007",
          "slug": "garlic-rice",
          "name": "Garlic Rice",
          "photo": null,
          "servings": 4,
          "totalMinutes": 25
        }
      },
      "PairingsInput": {
        "type": "object",
        "properties": {
          "recipes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 100,
            "description": "Recipes it goes with, by id or slug."
          },
          "tag": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40,
            "description": "Or every recipe with this tag, now and later."
          }
        },
        "example": {
          "recipes": [
            "garlic-rice"
          ]
        }
      },
      "RecipeRatingInput": {
        "type": "object",
        "properties": {
          "rating": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 5,
            "description": "From 1 to 5 stars, or null to clear it."
          }
        },
        "required": [
          "rating"
        ],
        "example": {
          "rating": 4
        }
      },
      "Swaps": {
        "type": "object",
        "properties": {
          "swaps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Swap"
            }
          }
        },
        "required": [
          "swaps"
        ],
        "example": {
          "swaps": [
            {
              "lines": [
                "1 cup Greek yogurt"
              ],
              "note": "Stir in off the heat so it doesn't split; a little tangier.",
              "atHome": true,
              "steps": []
            }
          ]
        }
      },
      "Swap": {
        "type": "object",
        "properties": {
          "lines": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ingredient lines in its place, with amounts for the recipe as written."
          },
          "note": {
            "type": "string",
            "description": "How to use it, and how it changes the dish."
          },
          "atHome": {
            "type": "boolean",
            "description": "It's in the pantry."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "stepId": {
                  "type": "string"
                },
                "text": {
                  "type": "string",
                  "maxLength": 5000,
                  "description": "The step, rewritten."
                }
              },
              "required": [
                "stepId",
                "text"
              ]
            },
            "description": "Steps it changes, rewritten. Usually none."
          }
        },
        "required": [
          "lines",
          "note",
          "atHome",
          "steps"
        ],
        "example": {
          "lines": [
            "1 cup Greek yogurt"
          ],
          "note": "Stir in off the heat so it doesn't split; a little tangier.",
          "atHome": true,
          "steps": []
        }
      },
      "StandInInput": {
        "type": "object",
        "properties": {
          "ingredientId": {
            "type": "string",
            "example": "0199e8b0-5a10-7a01-8b00-000000000012"
          }
        },
        "required": [
          "ingredientId"
        ],
        "example": {
          "ingredientId": "0199e8b0-5a10-7a01-8b00-000000000012"
        }
      },
      "SavedSwap": {
        "type": "object",
        "properties": {
          "recipe": {
            "$ref": "#/components/schemas/Recipe"
          },
          "undoVersionId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The version of the recipe as it was, to restore to undo the swap."
          }
        },
        "required": [
          "recipe",
          "undoVersionId"
        ],
        "example": {
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken",
            "description": "Crisp chicken thighs roasted with lemon, garlic and thyme.",
            "servings": 4,
            "totalMinutes": 50,
            "rating": 5,
            "tags": [
              "chicken",
              "dinner"
            ],
            "diets": [
              "gluten-free",
              "dairy-free",
              "egg-free",
              "nut-free"
            ],
            "photo": {
              "full": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=full&v=4b1c2d3e",
              "card": "/api/recipes/0199e8b0-5a10-7a01-8b00-000000000006/photo?size=card&v=4b1c2d3e"
            },
            "lastMadeAt": "2026-10-09T01:15:00.000Z",
            "createdAt": "2026-10-01T17:30:00.000Z",
            "updatedAt": "2026-10-15T18:30:00.000Z",
            "origin": "link",
            "photoByAi": false,
            "sourceUrl": "https://example.com/lemon-chicken",
            "yieldText": null,
            "prepMinutes": 10,
            "cookMinutes": 40,
            "notes": "Bone-in thighs work too: give them 10 minutes more.",
            "dietOverrides": {},
            "needs": [],
            "nutrition": {
              "source": "calculated",
              "calories": 412,
              "protein": 31.5,
              "carbs": 6.2,
              "fat": 28.1,
              "fiber": 1.4,
              "sugar": 1.2,
              "sodium": 640,
              "missing": [],
              "servings": 4
            },
            "ingredients": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000011",
                "section": null,
                "text": "2 lb chicken thighs",
                "quantity": 2,
                "quantityMax": null,
                "unit": "pound",
                "food": "chicken thighs",
                "note": null,
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000012",
                "section": null,
                "text": "1 tbsp olive oil",
                "quantity": 1,
                "quantityMax": null,
                "unit": "tablespoon",
                "food": "olive oil",
                "note": null,
                "mark": "optional"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000013",
                "section": null,
                "text": "2 lemons, juiced",
                "quantity": 2,
                "quantityMax": null,
                "unit": null,
                "food": "lemons",
                "note": "juiced",
                "mark": "needed"
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000014",
                "section": null,
                "text": "4 cloves garlic, smashed",
                "quantity": 4,
                "quantityMax": null,
                "unit": "clove",
                "food": "garlic",
                "note": "smashed",
                "mark": "swappable"
              }
            ],
            "steps": [
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000015",
                "section": null,
                "text": "Heat the oven to 425°F."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000016",
                "section": null,
                "text": "Toss the chicken with the oil, lemon juice, garlic, salt and pepper."
              },
              {
                "id": "0199e8b0-5a10-7a01-8b00-000000000017",
                "section": null,
                "text": "Roast skin side up for 40 minutes, until crisp."
              }
            ]
          },
          "undoVersionId": "0199e8b0-5a10-7a01-8b00-00000000000d"
        }
      },
      "SaveSwapInput": {
        "type": "object",
        "properties": {
          "ingredientId": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            },
            "minItems": 1,
            "maxItems": 5
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "stepId": {
                  "type": "string"
                },
                "text": {
                  "type": "string",
                  "maxLength": 5000,
                  "description": "The step, rewritten."
                }
              },
              "required": [
                "stepId",
                "text"
              ]
            },
            "maxItems": 20
          }
        },
        "required": [
          "ingredientId",
          "lines"
        ],
        "example": {
          "ingredientId": "0199e8b0-5a10-7a01-8b00-000000000012",
          "lines": [
            "1 tbsp butter"
          ],
          "steps": []
        }
      },
      "RecipeProposal": {
        "type": "object",
        "properties": {
          "proposal": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Proposal"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "proposal"
        ],
        "example": {
          "proposal": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000029",
            "kind": "tidy",
            "status": "pending",
            "resolvedAt": null,
            "recipe": {
              "id": "0199e8b0-5a10-7a01-8b00-000000000006",
              "slug": "lemon-chicken",
              "name": "Lemon Chicken"
            },
            "summary": "Split a long step and marked the chicken as needed.",
            "sections": [
              "steps",
              "marks"
            ],
            "note": null,
            "payload": null,
            "chat": null,
            "before": {
              "steps": [
                "Rub the chicken with lemon and garlic, then roast it at 200°C for 40 minutes until golden. Enjoy!"
              ],
              "marks": [
                {
                  "line": "2 lb chicken thighs",
                  "mark": null
                },
                {
                  "line": "2 lemons, juiced",
                  "mark": null
                }
              ]
            },
            "after": {
              "steps": [
                "Rub the chicken with lemon and garlic.",
                "Roast at 200°C for 40 minutes, until golden."
              ],
              "marks": [
                {
                  "line": "2 lb chicken thighs",
                  "mark": "needed"
                },
                {
                  "line": "2 lemons, juiced",
                  "mark": "needed"
                }
              ]
            },
            "createdAt": "2026-10-01T17:30:00.000Z"
          }
        }
      },
      "TidyInput": {
        "type": "object",
        "properties": {
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TidySection"
            },
            "maxItems": 7,
            "description": "What to tidy; every section when left out."
          }
        },
        "example": {
          "sections": [
            "steps",
            "tags",
            "marks"
          ]
        }
      },
      "TidySection": {
        "type": "string",
        "enum": [
          "ingredients",
          "steps",
          "tags",
          "description",
          "details",
          "marks",
          "foods"
        ],
        "description": "A part of a recipe tidy up can change: its ingredients, steps, tags, a missing description, missing servings and times, and the marks on its ingredients."
      },
      "RecipeVersion": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "import",
              "edit",
              "tidy",
              "assistant",
              "swap",
              "restore"
            ]
          },
          "name": {
            "type": "string",
            "description": "The recipe's name then."
          },
          "createdBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "source",
          "name",
          "createdBy",
          "createdAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000d",
          "source": "edit",
          "name": "Lemon Chicken",
          "createdBy": "Sam Rivera",
          "createdAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "HouseholdSettings": {
        "type": "object",
        "properties": {
          "settings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettingState"
            }
          }
        },
        "required": [
          "settings"
        ],
        "description": "No module has household settings yet, so the list is empty.",
        "example": {
          "settings": []
        }
      },
      "SettingState": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "example": "errorTracking.environment"
          },
          "kind": {
            "type": "string",
            "enum": [
              "text",
              "number",
              "switch",
              "choice",
              "list",
              "secret",
              "json"
            ]
          },
          "value": {
            "description": "Its value, or null for a secret or a setting that isn't set."
          },
          "default": {},
          "source": {
            "type": "string",
            "enum": [
              "default",
              "saved",
              "environment"
            ]
          },
          "lockedBy": {
            "type": [
              "string",
              "null"
            ],
            "description": "The environment variable that sets it, which means it can't be changed here.",
            "example": "MERRAN_SENTRY_ENVIRONMENT"
          },
          "secret": {
            "type": "object",
            "properties": {
              "set": {
                "type": "boolean"
              },
              "hint": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Its last four characters, when it's long enough."
              },
              "unreadable": {
                "type": "boolean",
                "description": "Sealed with another master key, so it needs entering again."
              }
            },
            "required": [
              "set",
              "hint",
              "unreadable"
            ]
          },
          "choices": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "key",
          "kind",
          "source",
          "lockedBy"
        ],
        "example": {
          "key": "errorTracking.environment",
          "kind": "text",
          "value": "home",
          "default": "production",
          "source": "saved",
          "lockedBy": null
        }
      },
      "SettingChanges": {
        "type": "object",
        "additionalProperties": {},
        "description": "New values by key. Null puts a setting back to its default, and deletes a secret.",
        "example": {
          "errorTracking.environment": "home",
          "errorTracking.dsn": null
        }
      },
      "ServerSettings": {
        "type": "object",
        "properties": {
          "about": {
            "type": "object",
            "properties": {
              "version": {
                "type": "string"
              },
              "installMethod": {
                "type": "string",
                "enum": [
                  "source",
                  "docker"
                ]
              },
              "database": {
                "type": "string",
                "enum": [
                  "sqlite",
                  "postgres"
                ]
              },
              "publicUrl": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "version",
              "installMethod",
              "database",
              "publicUrl"
            ]
          },
          "settings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SettingState"
            }
          },
          "video": {
            "type": "object",
            "properties": {
              "ytDlp": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ffmpeg": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "cookies": {
                "type": "boolean"
              }
            },
            "required": [
              "ytDlp",
              "ffmpeg",
              "cookies"
            ],
            "description": "Video import's tools, by their versions, or null when they aren't there; and whether there's a cookies.txt for sites that want a signed-in session."
          },
          "sites": {
            "type": "object",
            "properties": {
              "sites": {
                "type": "number"
              },
              "recipes": {
                "type": "number"
              },
              "classics": {
                "type": "object",
                "properties": {
                  "working": {
                    "type": "number"
                  },
                  "all": {
                    "type": "number"
                  }
                },
                "required": [
                  "working",
                  "all"
                ]
              },
              "readAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            },
            "required": [
              "sites",
              "recipes",
              "classics",
              "readAt"
            ],
            "description": "The recipe sites Merran reads, the recipes it has from them, how many of the starter collections' classics open, and when a site was last read."
          },
          "kroger": {
            "type": "object",
            "properties": {
              "redirectUri": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "redirectUri"
            ],
            "description": "The redirect address to give the Kroger developer app: null without a public address set, when it's this server's address and /api/stores/kroger/callback."
          }
        },
        "required": [
          "about",
          "settings",
          "video",
          "sites",
          "kroger"
        ],
        "example": {
          "about": {
            "version": "0.1.0",
            "installMethod": "docker",
            "database": "sqlite",
            "publicUrl": "https://merran.example.com"
          },
          "settings": [
            {
              "key": "errorTracking.environment",
              "kind": "text",
              "value": "home",
              "default": "production",
              "source": "saved",
              "lockedBy": null
            },
            {
              "key": "errorTracking.dsn",
              "kind": "secret",
              "value": null,
              "default": null,
              "source": "environment",
              "lockedBy": "MERRAN_SENTRY_DSN",
              "secret": {
                "set": true,
                "hint": "4f2a",
                "unreadable": false
              }
            }
          ],
          "video": {
            "ytDlp": "2026.08.19",
            "ffmpeg": "8.0",
            "cookies": false
          },
          "sites": {
            "sites": 12,
            "recipes": 214,
            "classics": {
              "working": 71,
              "all": 73
            },
            "readAt": "2026-10-15T18:30:00.000Z"
          },
          "kroger": {
            "redirectUri": "https://merran.example.com/api/stores/kroger/callback"
          }
        }
      },
      "ServerAi": {
        "type": "object",
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiKey"
            }
          },
          "policy": {
            "$ref": "#/components/schemas/AiPolicy"
          },
          "models": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiModelOption"
            },
            "description": "The models the server's keys can run."
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "anthropic",
                "openai",
                "google"
              ]
            }
          },
          "households": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "covered": {
                  "type": "boolean"
                },
                "serverAdmin": {
                  "type": "boolean",
                  "description": "A server admin is in it."
                },
                "limit": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "In US dollars."
                },
                "spent": {
                  "type": "number",
                  "description": "This month, on the server's keys."
                },
                "near": {
                  "type": "boolean"
                }
              },
              "required": [
                "id",
                "name",
                "covered",
                "serverAdmin",
                "limit",
                "spent",
                "near"
              ]
            },
            "description": "Every household, and its spend on the server's keys."
          },
          "usage": {
            "type": "object",
            "properties": {
              "months": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "month": {
                      "type": "string"
                    },
                    "cost": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "calls": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "month",
                    "cost",
                    "calls"
                  ]
                }
              },
              "models": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "provider": {
                      "type": "string",
                      "enum": [
                        "anthropic",
                        "openai",
                        "google",
                        "openai-compatible"
                      ]
                    },
                    "model": {
                      "type": "string"
                    },
                    "cost": {
                      "type": "number",
                      "description": "In US dollars."
                    },
                    "calls": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "provider",
                    "model",
                    "cost",
                    "calls"
                  ]
                }
              }
            },
            "required": [
              "months",
              "models"
            ]
          }
        },
        "required": [
          "keys",
          "policy",
          "models",
          "providers",
          "households",
          "usage"
        ],
        "example": {
          "keys": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000028",
              "provider": "anthropic",
              "name": null,
              "baseUrl": null,
              "hint": "7xQa",
              "models": [
                "claude-haiku-5-5",
                "claude-opus-5-5"
              ],
              "testedAt": "2026-10-16T17:00:00.000Z",
              "fromEnvironment": false,
              "unreadable": false,
              "credit": {
                "amount": 20,
                "since": "2026-10-01T17:00:00.000Z",
                "spent": 16.6,
                "left": 3.4
              },
              "outOfCreditAt": null,
              "lowOnCredit": true
            }
          ],
          "policy": {
            "serverKeysFor": "admins",
            "households": [],
            "monthlyLimit": null,
            "limits": {},
            "tasks": [
              "assistant",
              "import",
              "tidy",
              "notes",
              "swaps",
              "nutrition",
              "shopping",
              "receipts",
              "discover",
              "reminders"
            ],
            "models": [],
            "householdKeys": true,
            "householdAddresses": false,
            "large": null,
            "small": null
          },
          "models": [
            {
              "ref": "anthropic:claude-opus-5-5",
              "name": "Claude Opus 5.5",
              "size": "large",
              "price": [
                4,
                20
              ],
              "source": "server"
            }
          ],
          "providers": [
            "anthropic",
            "openai",
            "google"
          ],
          "households": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000003",
              "name": "The Johnsons",
              "covered": true,
              "serverAdmin": true,
              "limit": null,
              "spent": 1.24,
              "near": false
            }
          ],
          "usage": {
            "months": [
              {
                "month": "2026-10",
                "cost": 1.24,
                "calls": 31
              }
            ],
            "models": [
              {
                "provider": "anthropic",
                "model": "claude-opus-5-5",
                "cost": 1.24,
                "calls": 31
              }
            ]
          }
        }
      },
      "AiPolicy": {
        "type": "object",
        "properties": {
          "serverKeysFor": {
            "type": "string",
            "enum": [
              "admins",
              "chosen",
              "all",
              "none"
            ]
          },
          "households": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "maxItems": 1000
          },
          "monthlyLimit": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000
          },
          "limits": {
            "type": "object",
            "additionalProperties": {
              "type": "number",
              "minimum": 0,
              "maximum": 100000
            }
          },
          "tasks": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "assistant",
                "import",
                "tidy",
                "notes",
                "swaps",
                "nutrition",
                "shopping",
                "receipts",
                "discover",
                "reminders"
              ]
            },
            "maxItems": 10
          },
          "models": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 300
            },
            "maxItems": 100
          },
          "householdKeys": {
            "type": "boolean"
          },
          "householdAddresses": {
            "type": "boolean"
          },
          "large": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300
          },
          "small": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300
          }
        },
        "required": [
          "serverKeysFor",
          "households",
          "monthlyLimit",
          "limits",
          "tasks",
          "models",
          "householdKeys",
          "householdAddresses",
          "large",
          "small"
        ],
        "description": "Who may use the server's keys: `serverKeysFor` is admins, chosen, all, none (the server admins' households, the chosen `households`, every household, or none). `monthlyLimit` is each household's in US dollars on the server's keys (null for none), and `limits` a household's own instead. `tasks` and `models` are what the server's keys cover (no models means any). `householdKeys` and `householdAddresses` say whether households may add their own keys and addresses. `large` and `small` are the models households use when they haven't picked.",
        "example": {
          "serverKeysFor": "admins",
          "households": [],
          "monthlyLimit": 5,
          "limits": {},
          "tasks": [
            "assistant",
            "import",
            "tidy",
            "notes",
            "swaps",
            "nutrition",
            "shopping",
            "receipts",
            "discover",
            "reminders"
          ],
          "models": [],
          "householdKeys": true,
          "householdAddresses": false,
          "large": null,
          "small": null
        }
      },
      "JobList": {
        "type": "object",
        "properties": {
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobListItem"
            }
          }
        },
        "required": [
          "jobs"
        ],
        "example": {
          "jobs": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
              "kind": "household.import",
              "status": "running",
              "scheduled": false,
              "householdId": "0199e8b0-5a10-7a01-8b00-000000000003",
              "progress": {
                "done": 42,
                "total": 128
              },
              "result": null,
              "error": null,
              "attempts": 1,
              "maxAttempts": 3,
              "runAt": "2026-10-15T18:30:00.000Z",
              "startedAt": "2026-10-15T18:30:00.000Z",
              "finishedAt": null,
              "createdAt": "2026-10-15T18:30:00.000Z",
              "householdName": "The Riveras"
            }
          ]
        }
      },
      "JobListItem": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Job"
          },
          {
            "type": "object",
            "properties": {
              "householdName": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "householdName"
            ]
          }
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-00000000000b",
          "kind": "household.import",
          "status": "running",
          "scheduled": false,
          "householdId": "0199e8b0-5a10-7a01-8b00-000000000003",
          "progress": {
            "done": 42,
            "total": 128
          },
          "result": null,
          "error": null,
          "attempts": 1,
          "maxAttempts": 3,
          "runAt": "2026-10-15T18:30:00.000Z",
          "startedAt": "2026-10-15T18:30:00.000Z",
          "finishedAt": null,
          "createdAt": "2026-10-15T18:30:00.000Z",
          "householdName": "The Riveras"
        }
      },
      "KrogerAppTested": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "ok"
        ],
        "example": {
          "ok": true
        }
      },
      "KrogerAppTest": {
        "type": "object",
        "properties": {
          "clientId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "clientSecret": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          }
        },
        "description": "A Kroger developer app's client ID and secret to test before saving them. Either left out is the saved one.",
        "example": {
          "clientId": "merran-abc123"
        }
      },
      "UsdaKeyTested": {
        "type": "object",
        "properties": {
          "found": {
            "type": "string",
            "description": "What a search for apples found."
          }
        },
        "required": [
          "found"
        ],
        "example": {
          "found": "Apples, raw, with skin"
        }
      },
      "UsdaKeyTest": {
        "type": "object",
        "properties": {
          "usdaKey": {
            "type": "string",
            "minLength": 10,
            "maxLength": 200,
            "description": "A key to try before saving it; the saved one when left out."
          }
        },
        "example": {}
      },
      "Updates": {
        "type": "object",
        "properties": {
          "current": {
            "type": "string",
            "description": "The version running."
          },
          "devBuild": {
            "type": "boolean",
            "description": "Not a release (from source, or the edge image), so it isn't checked."
          },
          "checking": {
            "type": "boolean",
            "description": "It looks every 6 hours: on unless MERRAN_UPDATE_CHECK says no."
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why the last look didn't work, if it didn't."
          },
          "latest": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "version": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string"
              },
              "notes": {
                "type": "string",
                "description": "In Markdown."
              },
              "publishedAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "prerelease": {
                "type": "boolean"
              }
            },
            "required": [
              "version",
              "name",
              "url",
              "notes",
              "publishedAt",
              "prerelease"
            ],
            "description": "The newest release this server would update to: pre-releases too when it runs one."
          },
          "newer": {
            "type": "boolean",
            "description": "The latest is newer than what's running."
          },
          "installMethod": {
            "type": "string",
            "enum": [
              "source",
              "docker"
            ]
          }
        },
        "required": [
          "current",
          "devBuild",
          "checking",
          "checkedAt",
          "error",
          "latest",
          "newer",
          "installMethod"
        ],
        "example": {
          "current": "0.1.0-alpha.1",
          "devBuild": false,
          "checking": true,
          "checkedAt": "2026-10-20T18:53:00.000Z",
          "error": null,
          "latest": {
            "version": "0.1.0-alpha.2",
            "name": "Merran 0.1.0-alpha.2",
            "url": "https://github.com/merran-app/merran/releases/tag/v0.1.0-alpha.2",
            "notes": "## What's new\n\n- Send splits the list across your stores.",
            "publishedAt": "2026-10-20T16:00:00Z",
            "prerelease": true
          },
          "newer": true,
          "installMethod": "docker"
        }
      },
      "VoiceSettings": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "device",
              "kokoro",
              "openai"
            ]
          },
          "kokoroUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "voices": {
            "type": "object",
            "properties": {
              "kokoro": {
                "type": "string"
              },
              "openai": {
                "type": "string"
              }
            },
            "required": [
              "kokoro",
              "openai"
            ],
            "description": "The voice picked for each."
          },
          "kokoro": {
            "type": "object",
            "properties": {
              "voices": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "accent": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "gender": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "woman",
                        "man",
                        null
                      ]
                    },
                    "english": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "accent",
                    "gender",
                    "english"
                  ]
                }
              },
              "error": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Why Kokoro couldn't say which voices it has."
              }
            },
            "required": [
              "voices",
              "error"
            ]
          },
          "openai": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "name"
              ]
            }
          }
        },
        "required": [
          "provider",
          "kokoroUrl",
          "voices",
          "kokoro",
          "openai"
        ],
        "example": {
          "provider": "kokoro",
          "kokoroUrl": "http://kokoro:8880",
          "voices": {
            "kokoro": "af_heart",
            "openai": "marin"
          },
          "kokoro": {
            "voices": [
              {
                "id": "af_heart",
                "name": "Heart",
                "accent": "American",
                "gender": "woman",
                "english": true
              }
            ],
            "error": null
          },
          "openai": [
            {
              "id": "marin",
              "name": "Marin"
            }
          ]
        }
      },
      "VoicePreview": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "kokoro",
              "openai"
            ]
          },
          "voice": {
            "type": "string",
            "pattern": "^[a-z0-9_-]{1,40}$/i"
          },
          "kokoroUrl": {
            "type": "string",
            "maxLength": 500,
            "format": "uri",
            "description": "Kokoro's address, to try one before saving it."
          }
        },
        "required": [
          "provider",
          "voice"
        ],
        "example": {
          "provider": "kokoro",
          "voice": "bf_emma"
        }
      },
      "ShareOutcome": {
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "added",
              "already",
              "importing",
              "receipt",
              "failed"
            ],
            "description": "Added; already in the library (this is that recipe); several links, now importing in the background; a receipt, read and waiting for a look before it goes in the pantry; or not added."
          },
          "code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it wasn't added, for programs: \"no_recipe\", \"no_recipe_data\", \"ai_noKey\" and the like."
          },
          "message": {
            "type": "string",
            "description": "What happened, in words to show, as a Shortcut does.",
            "example": "Added Lemon Chicken to Merran."
          },
          "recipe": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "slug": {
                "type": "string"
              },
              "name": {
                "type": "string"
              }
            },
            "required": [
              "id",
              "slug",
              "name"
            ]
          },
          "receiptId": {
            "type": [
              "string",
              "null"
            ],
            "description": "A receipt: the one to look over, at /pantry/receipts/{receiptId}."
          },
          "importId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Several links: the import, to follow at /recipes/import/links/{importId}."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The recipe's page, or the import's."
          }
        },
        "required": [
          "outcome",
          "code",
          "message",
          "recipe",
          "receiptId",
          "importId",
          "url"
        ],
        "example": {
          "outcome": "added",
          "code": null,
          "message": "Added Lemon Chicken to Merran.",
          "recipe": {
            "id": "0199e8b0-5a10-7a01-8b00-000000000006",
            "slug": "lemon-chicken",
            "name": "Lemon Chicken"
          },
          "importId": null,
          "receiptId": null,
          "url": "https://merran.example.com/recipes/lemon-chicken"
        }
      },
      "SharedText": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "example": {
          "title": "Lemon Chicken",
          "text": "Making this tonight! https://example.com/lemon-chicken"
        }
      },
      "SiteList": {
        "type": "object",
        "properties": {
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FollowedSite"
            }
          },
          "suggestions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "description": "Follow it with this."
                },
                "description": {
                  "type": "string"
                }
              },
              "required": [
                "name",
                "url",
                "description"
              ]
            },
            "description": "Sites that work well with Merran, not followed yet."
          }
        },
        "required": [
          "sites",
          "suggestions"
        ],
        "example": {
          "sites": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000030",
              "name": "RecipeTin Eats",
              "url": "https://www.recipetineats.com/",
              "feedUrl": "https://www.recipetineats.com/feed/",
              "recipes": [
                {
                  "id": "0199e8b0-5a10-7a01-8b00-000000000031",
                  "url": "https://www.recipetineats.com/honey-garlic-chicken/",
                  "name": "Honey Garlic Chicken",
                  "site": "RecipeTin Eats",
                  "description": "Juicy chicken in a sticky honey garlic sauce, in 12 minutes.",
                  "minutes": 12,
                  "courses": [
                    "main"
                  ],
                  "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
                  "publishedAt": "2026-10-01T17:30:00.000Z",
                  "inLibrary": null,
                  "suits": true
                },
                {
                  "id": "0199e8b0-5a10-7a01-8b00-000000000032",
                  "url": "https://www.recipetineats.com/lemon-chicken/",
                  "name": "Lemon Chicken",
                  "site": "RecipeTin Eats",
                  "description": null,
                  "minutes": 35,
                  "courses": [
                    "main"
                  ],
                  "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
                  "publishedAt": "2026-10-01T17:30:00.000Z",
                  "inLibrary": {
                    "slug": "lemon-chicken",
                    "name": "Lemon Chicken"
                  },
                  "suits": true
                }
              ],
              "waiting": 3,
              "reading": false,
              "readAt": "2026-10-15T18:30:00.000Z",
              "error": null,
              "followedAt": "2026-10-01T17:30:00.000Z"
            }
          ],
          "suggestions": [
            {
              "name": "Love and Lemons",
              "url": "https://www.loveandlemons.com/",
              "description": "Bright, seasonal vegetarian cooking."
            }
          ]
        }
      },
      "FollowedSite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "feedUrl": {
            "type": "string"
          },
          "recipes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ShelfRecipe"
            },
            "description": "Its newest recipes, newest first: posts Merran opened and can import."
          },
          "waiting": {
            "type": "number",
            "description": "New posts not opened yet: Merran opens them a few seconds apart."
          },
          "reading": {
            "type": "boolean",
            "description": "Not read for the first time yet."
          },
          "readAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why its feed couldn't be read last time."
          },
          "followedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "url",
          "feedUrl",
          "recipes",
          "waiting",
          "reading",
          "readAt",
          "error",
          "followedAt"
        ],
        "example": {
          "id": "0199e8b0-5a10-7a01-8b00-000000000030",
          "name": "RecipeTin Eats",
          "url": "https://www.recipetineats.com/",
          "feedUrl": "https://www.recipetineats.com/feed/",
          "recipes": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000031",
              "url": "https://www.recipetineats.com/honey-garlic-chicken/",
              "name": "Honey Garlic Chicken",
              "site": "RecipeTin Eats",
              "description": "Juicy chicken in a sticky honey garlic sauce, in 12 minutes.",
              "minutes": 12,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": null,
              "suits": true
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000032",
              "url": "https://www.recipetineats.com/lemon-chicken/",
              "name": "Lemon Chicken",
              "site": "RecipeTin Eats",
              "description": null,
              "minutes": 35,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": {
                "slug": "lemon-chicken",
                "name": "Lemon Chicken"
              },
              "suits": true
            }
          ],
          "waiting": 3,
          "reading": false,
          "readAt": "2026-10-15T18:30:00.000Z",
          "error": null,
          "followedAt": "2026-10-01T17:30:00.000Z"
        }
      },
      "FollowSite": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 3,
            "maxLength": 500,
            "description": "Any page of the site, its feed, or a suggestion's `url`."
          }
        },
        "required": [
          "url"
        ],
        "example": {
          "url": "https://www.loveandlemons.com/"
        }
      },
      "SiteRecipes": {
        "type": "object",
        "properties": {
          "recipes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ShelfRecipe"
            }
          }
        },
        "required": [
          "recipes"
        ],
        "example": {
          "recipes": [
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000031",
              "url": "https://www.recipetineats.com/honey-garlic-chicken/",
              "name": "Honey Garlic Chicken",
              "site": "RecipeTin Eats",
              "description": "Juicy chicken in a sticky honey garlic sauce, in 12 minutes.",
              "minutes": 12,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": null,
              "suits": true
            },
            {
              "id": "0199e8b0-5a10-7a01-8b00-000000000032",
              "url": "https://www.recipetineats.com/lemon-chicken/",
              "name": "Lemon Chicken",
              "site": "RecipeTin Eats",
              "description": null,
              "minutes": 35,
              "courses": [
                "main"
              ],
              "photo": "/api/sites/recipes/0199e8b0-5a10-7a01-8b00-000000000031/photo",
              "publishedAt": "2026-10-01T17:30:00.000Z",
              "inLibrary": {
                "slug": "lemon-chicken",
                "name": "Lemon Chicken"
              },
              "suits": true
            }
          ]
        }
      },
      "StoresStatus": {
        "type": "object",
        "properties": {
          "kroger": {
            "type": "object",
            "properties": {
              "ready": {
                "type": "boolean",
                "description": "The server has a Kroger developer app."
              },
              "location": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/StoreLocation"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The household's store, or null."
              },
              "modality": {
                "type": "string",
                "enum": [
                  "PICKUP",
                  "DELIVERY"
                ],
                "description": "Orders are picked up or delivered."
              },
              "linked": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "by": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "at": {
                    "type": "string",
                    "format": "date-time"
                  }
                },
                "required": [
                  "by",
                  "at"
                ],
                "description": "The account whose cart things go to."
              },
              "needsRelink": {
                "type": "boolean",
                "description": "Kroger stopped taking the linked account."
              },
              "pricesCheckedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When its prices and sales were last checked."
              },
              "leanOnSales": {
                "type": "string",
                "enum": [
                  "off",
                  "some",
                  "lots"
                ],
                "description": "How much Fill the week and the autoplan lean on deals: the household setting `stores.leanOnSales`."
              },
              "weeklyBudget": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The household's weekly budget in its store's currency, or null: the household setting `stores.weeklyBudget`."
              }
            },
            "required": [
              "ready",
              "location",
              "modality",
              "linked",
              "needsRelink",
              "pricesCheckedAt",
              "leanOnSales",
              "weeklyBudget"
            ]
          }
        },
        "required": [
          "kroger"
        ],
        "example": {
          "kroger": {
            "ready": true,
            "location": {
              "locationId": "70500822",
              "name": "Fry's Food Stores - Phoenix",
              "banner": "Fry's",
              "domain": "frysfood.com",
              "address": "4724 N 20th St, Phoenix, AZ"
            },
            "modality": "PICKUP",
            "linked": {
              "by": "Kyle",
              "at": "2026-10-01T17:30:00.000Z"
            },
            "needsRelink": false,
            "pricesCheckedAt": null,
            "leanOnSales": "some",
            "weeklyBudget": 120
          }
        }
      },
      "StoreLocation": {
        "type": "object",
        "properties": {
          "locationId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "banner": {
            "type": "string",
            "description": "Kroger's name for the chain: Fry's, Ralphs, King Soopers..."
          },
          "domain": {
            "type": "string",
            "description": "The banner's site."
          },
          "address": {
            "type": "string"
          }
        },
        "required": [
          "locationId",
          "name",
          "banner",
          "domain",
          "address"
        ],
        "example": {
          "locationId": "70500822",
          "name": "Fry's Food Stores - Phoenix",
          "banner": "Fry's",
          "domain": "frysfood.com",
          "address": "4724 N 20th St, Phoenix, AZ"
        }
      },
      "Deals": {
        "type": "object",
        "properties": {
          "store": {
            "type": [
              "string",
              "null"
            ],
            "description": "The household's store's banner, or null without one."
          },
          "checkedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "leanOnSales": {
            "type": "string",
            "enum": [
              "off",
              "some",
              "lots"
            ]
          },
          "deals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "food": {
                  "type": "string",
                  "description": "The food: chicken thigh."
                },
                "product": {
                  "$ref": "#/components/schemas/StoreProduct"
                },
                "verdict": {
                  "$ref": "#/components/schemas/SaleVerdict"
                },
                "usual": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "off": {
                  "type": "number",
                  "description": "How much is off: 0.25."
                }
              },
              "required": [
                "food",
                "product",
                "verdict",
                "usual",
                "off"
              ]
            }
          },
          "recipes": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "verdict": {
                    "$ref": "#/components/schemas/SaleVerdict"
                  }
                },
                "required": [
                  "name",
                  "verdict"
                ]
              }
            },
            "description": "By recipe id, what each recipe uses that's on sale, named as the recipe has it."
          }
        },
        "required": [
          "store",
          "checkedAt",
          "leanOnSales",
          "deals",
          "recipes"
        ],
        "example": {
          "store": "Fry's",
          "checkedAt": "2026-10-01T17:30:00.000Z",
          "leanOnSales": "some",
          "deals": [
            {
              "food": "milk",
              "product": {
                "id": "0001111041700",
                "upc": "0001111041700",
                "name": "Kroger 2% Reduced Fat Milk",
                "brand": "Kroger",
                "size": "1 gal",
                "image": "https://www.kroger.com/product/images/medium/front/0001111041700",
                "price": 349,
                "salePrice": 299,
                "saleEnds": "2026-10-21",
                "currency": "USD",
                "aisle": "Dairy",
                "inStock": true,
                "url": "https://www.frysfood.com/p/milk/0001111041700"
              },
              "verdict": "sale",
              "usual": 349,
              "off": 0.14
            }
          ],
          "recipes": {
            "0199e8b0-5a10-7a01-8b00-000000000006": [
              {
                "name": "milk",
                "verdict": "sale"
              }
            ]
          }
        }
      },
      "StoreProduct": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "upc": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": "string"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ]
          },
          "size": {
            "type": [
              "string",
              "null"
            ]
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "price": {
            "type": [
              "integer",
              "null"
            ],
            "description": "In the currency's smallest unit: cents."
          },
          "salePrice": {
            "type": [
              "integer",
              "null"
            ]
          },
          "saleEnds": {
            "type": [
              "string",
              "null"
            ],
            "description": "The sale's last day."
          },
          "currency": {
            "type": "string"
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ]
          },
          "inStock": {
            "type": "boolean"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "upc",
          "name",
          "brand",
          "size",
          "image",
          "price",
          "salePrice",
          "saleEnds",
          "currency",
          "aisle",
          "inStock",
          "url"
        ],
        "example": {
          "id": "0001111041700",
          "upc": "0001111041700",
          "name": "Kroger 2% Reduced Fat Milk",
          "brand": "Kroger",
          "size": "1 gal",
          "image": "https://www.kroger.com/product/images/medium/front/0001111041700",
          "price": 349,
          "salePrice": 299,
          "saleEnds": "2026-10-21",
          "currency": "USD",
          "aisle": "Dairy",
          "inStock": true,
          "url": "https://www.frysfood.com/p/milk/0001111041700"
        }
      },
      "SaleVerdict": {
        "type": "string",
        "enum": [
          "lowest",
          "sale",
          "small"
        ],
        "description": "How good a sale is, against what the product usually costs: the lowest in a while, a normal sale, or barely off (under a tenth)."
      },
      "RecipeCosts": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string"
          },
          "weeklyBudget": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The household's weekly budget, in cents, or null."
          },
          "recipes": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "total": {
                  "type": "integer",
                  "description": "In cents."
                },
                "perServing": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "priced": {
                  "type": "integer",
                  "description": "Its lines with a price."
                },
                "counted": {
                  "type": "integer",
                  "description": "Its lines with an amount of something bought."
                }
              },
              "required": [
                "total",
                "perServing",
                "priced",
                "counted"
              ]
            },
            "description": "By recipe id, the recipes with a cost worth showing: three in five of their lines priced."
          }
        },
        "required": [
          "currency",
          "weeklyBudget",
          "recipes"
        ],
        "example": {
          "currency": "USD",
          "weeklyBudget": 12000,
          "recipes": {
            "0199e8b0-5a10-7a01-8b00-000000000006": {
              "total": 1240,
              "perServing": 310,
              "priced": 5,
              "counted": 6
            }
          }
        }
      },
      "ListPrices": {
        "type": "object",
        "properties": {
          "store": {
            "type": [
              "string",
              "null"
            ],
            "description": "The household's Kroger store's banner, or null without one."
          },
          "currency": {
            "type": "string"
          },
          "items": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "properties": {
                "store": {
                  "type": "string",
                  "description": "The store it's priced at: where the household picked a product for it, newest first, else its Kroger store."
                },
                "total": {
                  "type": "integer",
                  "description": "In cents, for the packages needed, at the sale price when there's a sale."
                },
                "packages": {
                  "type": "integer"
                },
                "sure": {
                  "type": "boolean",
                  "description": "How many is worked out from the product's size, not a guess."
                },
                "sale": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "verdict": {
                      "type": "string",
                      "enum": [
                        "lowest",
                        "sale"
                      ]
                    },
                    "saved": {
                      "type": "integer",
                      "description": "What it takes off the packages needed, in cents."
                    },
                    "ends": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    }
                  },
                  "required": [
                    "verdict",
                    "saved",
                    "ends"
                  ],
                  "description": "A real deal on it today; one barely off is left out."
                },
                "usual": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "description": "What a package usually costs, in cents, when it's on sale."
                },
                "product": {
                  "type": "string",
                  "description": "The product priced."
                },
                "seenOn": {
                  "type": "string",
                  "format": "date",
                  "description": "The day the price was seen."
                }
              },
              "required": [
                "store",
                "total",
                "packages",
                "sure",
                "sale",
                "usual",
                "product",
                "seenOn"
              ]
            },
            "description": "By list item id, what's left to get on every list, each at its store. Things with no product kept for them at a store, or no price, are left out."
          }
        },
        "required": [
          "store",
          "currency",
          "items"
        ],
        "example": {
          "store": "Fry's",
          "currency": "USD",
          "items": {
            "0199e8b0-5a10-7a01-8b00-00000000001d": {
              "store": "Fry's",
              "total": 598,
              "packages": 2,
              "sure": true,
              "sale": {
                "verdict": "sale",
                "saved": 100,
                "ends": "2026-10-21"
              },
              "usual": 349,
              "product": "Kroger 2% Reduced Fat Milk",
              "seenOn": "2026-10-17"
            }
          }
        }
      },
      "YourStores": {
        "type": "object",
        "properties": {
          "stores": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string",
                  "description": "The store's key: \"kroger\"."
                },
                "name": {
                  "type": "string",
                  "description": "As the household knows it: Fry's."
                },
                "connected": {
                  "type": "boolean",
                  "description": "Merran can find its products and send to its cart: the household's Kroger store, and Walmart when the server has it on."
                },
                "searchable": {
                  "type": "boolean",
                  "description": "Its site can be searched, in the household's country."
                }
              },
              "required": [
                "key",
                "name",
                "connected",
                "searchable"
              ]
            }
          }
        },
        "required": [
          "stores"
        ],
        "example": {
          "stores": [
            {
              "key": "kroger",
              "name": "Fry's",
              "connected": true,
              "searchable": true
            },
            {
              "key": "costco",
              "name": "Costco",
              "connected": false,
              "searchable": true
            }
          ]
        }
      },
      "SendPlan": {
        "type": "object",
        "properties": {
          "stores": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "key",
                "name"
              ]
            },
            "description": "The stores Merran can send to."
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "amount": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "picked": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "The stores the household picked a product at for its food (someone picked it, or it was sent there before; Merran's own picks don't count), newest first."
                },
                "sent": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "The stores it's been sent to while it's on the list."
                }
              },
              "required": [
                "itemId",
                "name",
                "amount",
                "picked",
                "sent"
              ]
            }
          }
        },
        "required": [
          "stores",
          "items"
        ],
        "example": {
          "stores": [
            {
              "key": "kroger",
              "name": "Fry's"
            },
            {
              "key": "walmart",
              "name": "Walmart"
            }
          ],
          "items": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "name": "milk",
              "amount": "1 gallon",
              "picked": [
                "walmart",
                "kroger"
              ],
              "sent": []
            },
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001e",
              "name": "eggs",
              "amount": "12",
              "picked": [],
              "sent": []
            }
          ]
        }
      },
      "KrogerStoreChanges": {
        "type": "object",
        "properties": {
          "locationId": {
            "type": "string",
            "maxLength": 40
          },
          "modality": {
            "type": "string",
            "enum": [
              "PICKUP",
              "DELIVERY"
            ]
          }
        },
        "example": {
          "locationId": "70500822",
          "modality": "PICKUP"
        }
      },
      "StoreLocations": {
        "type": "object",
        "properties": {
          "locations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StoreLocation"
            }
          }
        },
        "required": [
          "locations"
        ],
        "example": {
          "locations": [
            {
              "locationId": "70500822",
              "name": "Fry's Food Stores - Phoenix",
              "banner": "Fry's",
              "domain": "frysfood.com",
              "address": "4724 N 20th St, Phoenix, AZ"
            }
          ]
        }
      },
      "KrogerLink": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Where to send the person."
          }
        },
        "required": [
          "url"
        ],
        "example": {
          "url": "https://api.kroger.com/v1/connect/oauth2/authorize?..."
        }
      },
      "Basket": {
        "type": "object",
        "properties": {
          "store": {
            "type": "string",
            "description": "The banner: Fry's."
          },
          "location": {
            "$ref": "#/components/schemas/StoreLocation"
          },
          "modality": {
            "type": "string",
            "enum": [
              "PICKUP",
              "DELIVERY"
            ]
          },
          "linked": {
            "type": "boolean",
            "description": "An account is linked, so it can be sent."
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BasketLine"
            }
          }
        },
        "required": [
          "store",
          "location",
          "modality",
          "linked",
          "sentAt",
          "lines"
        ],
        "example": {
          "store": "Fry's",
          "location": {
            "locationId": "70500822",
            "name": "Fry's Food Stores - Phoenix",
            "banner": "Fry's",
            "domain": "frysfood.com",
            "address": "4724 N 20th St, Phoenix, AZ"
          },
          "modality": "PICKUP",
          "linked": true,
          "sentAt": null,
          "lines": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "name": "milk",
              "amount": "1 gallon",
              "product": {
                "id": "0001111041700",
                "upc": "0001111041700",
                "name": "Kroger 2% Reduced Fat Milk",
                "brand": "Kroger",
                "size": "1 gal",
                "image": "https://www.kroger.com/product/images/medium/front/0001111041700",
                "price": 349,
                "salePrice": 299,
                "saleEnds": "2026-10-21",
                "currency": "USD",
                "aisle": "Dairy",
                "inStock": true,
                "url": "https://www.frysfood.com/p/milk/0001111041700"
              },
              "suggested": true,
              "packages": 1,
              "sure": true,
              "sent": false,
              "sale": {
                "verdict": "sale",
                "usual": 349
              }
            }
          ]
        }
      },
      "BasketLine": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "amount": {
            "type": "string"
          },
          "product": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/StoreProduct"
              },
              {
                "type": "null"
              }
            ]
          },
          "suggested": {
            "type": "boolean",
            "description": "Merran's pick, not one the household chose."
          },
          "packages": {
            "type": "integer"
          },
          "sure": {
            "type": "boolean",
            "description": "How many is worked out from the product's size, not a guess."
          },
          "sent": {
            "type": "boolean",
            "description": "Sent to the cart last time."
          },
          "sale": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "verdict": {
                "$ref": "#/components/schemas/SaleVerdict"
              },
              "usual": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What it usually costs, in cents: its most common price over 90 days, else its regular price."
              }
            },
            "required": [
              "verdict",
              "usual"
            ],
            "description": "Its sale today, if it's on one."
          }
        },
        "required": [
          "itemId",
          "name",
          "amount",
          "product",
          "suggested",
          "packages",
          "sure",
          "sent",
          "sale"
        ],
        "example": {
          "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
          "name": "milk",
          "amount": "1 gallon",
          "product": {
            "id": "0001111041700",
            "upc": "0001111041700",
            "name": "Kroger 2% Reduced Fat Milk",
            "brand": "Kroger",
            "size": "1 gal",
            "image": "https://www.kroger.com/product/images/medium/front/0001111041700",
            "price": 349,
            "salePrice": 299,
            "saleEnds": "2026-10-21",
            "currency": "USD",
            "aisle": "Dairy",
            "inStock": true,
            "url": "https://www.frysfood.com/p/milk/0001111041700"
          },
          "suggested": true,
          "packages": 1,
          "sure": true,
          "sent": false,
          "sale": {
            "verdict": "sale",
            "usual": 349
          }
        }
      },
      "StoreProducts": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StoreProduct"
            }
          }
        },
        "required": [
          "products"
        ],
        "example": {
          "products": [
            {
              "id": "0001111041700",
              "upc": "0001111041700",
              "name": "Kroger 2% Reduced Fat Milk",
              "brand": "Kroger",
              "size": "1 gal",
              "image": "https://www.kroger.com/product/images/medium/front/0001111041700",
              "price": 349,
              "salePrice": 299,
              "saleEnds": "2026-10-21",
              "currency": "USD",
              "aisle": "Dairy",
              "inStock": true,
              "url": "https://www.frysfood.com/p/milk/0001111041700"
            }
          ]
        }
      },
      "PickProduct": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 40
          }
        },
        "required": [
          "productId"
        ],
        "example": {
          "productId": "0001111041700"
        }
      },
      "SentBasket": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "integer"
          },
          "store": {
            "type": "string"
          },
          "cartUrl": {
            "type": "string",
            "description": "The store's cart, to check out."
          }
        },
        "required": [
          "sent",
          "store",
          "cartUrl"
        ],
        "example": {
          "sent": 1,
          "store": "Fry's",
          "cartUrl": "https://www.frysfood.com/cart"
        }
      },
      "SendBasket": {
        "type": "object",
        "properties": {
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "packages": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 99
                }
              },
              "required": [
                "itemId",
                "packages"
              ]
            },
            "minItems": 1,
            "maxItems": 300
          }
        },
        "required": [
          "lines"
        ],
        "example": {
          "lines": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "packages": 1
            }
          ]
        }
      },
      "PriceCheck": {
        "type": "object",
        "properties": {
          "refreshed": {
            "type": "integer",
            "description": "Products the household keeps, given today's price."
          },
          "searched": {
            "type": "integer",
            "description": "Foods from its recipes looked up at the store."
          },
          "deals": {
            "type": "integer",
            "description": "Foods in its recipes that are a deal now."
          }
        },
        "required": [
          "refreshed",
          "searched",
          "deals"
        ],
        "example": {
          "refreshed": 12,
          "searched": 20,
          "deals": 3
        }
      },
      "WalmartBasket": {
        "type": "object",
        "properties": {
          "store": {
            "type": "string",
            "description": "Walmart."
          },
          "cartUrl": {
            "type": "string",
            "description": "Walmart's cart, to check out."
          },
          "sentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BasketLine"
            }
          },
          "toFind": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "words": {
                  "type": "string",
                  "description": "What to search Walmart for: the food as it's bought."
                }
              },
              "required": [
                "itemId",
                "words"
              ]
            },
            "description": "Things without a product yet, one for each food, for the extension to look for at Walmart."
          }
        },
        "required": [
          "store",
          "cartUrl",
          "sentAt",
          "lines",
          "toFind"
        ],
        "example": {
          "store": "Walmart",
          "cartUrl": "https://www.walmart.com/cart",
          "sentAt": null,
          "lines": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "name": "milk",
              "amount": "1 gallon",
              "product": {
                "id": "10450114",
                "upc": null,
                "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
                "brand": "Great Value",
                "size": "128 fl oz",
                "image": "https://i5.walmartimages.com/seo/milk.jpeg",
                "price": 327,
                "salePrice": null,
                "saleEnds": null,
                "currency": "USD",
                "aisle": null,
                "inStock": true,
                "url": "https://www.walmart.com/ip/10450114"
              },
              "suggested": true,
              "packages": 1,
              "sure": true,
              "sent": false,
              "sale": null
            },
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001e",
              "name": "eggs",
              "amount": "12",
              "product": null,
              "suggested": true,
              "packages": 1,
              "sure": true,
              "sent": false,
              "sale": null
            }
          ],
          "toFind": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001e",
              "words": "eggs"
            }
          ]
        }
      },
      "WalmartFound": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "products": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WalmartProduct"
                  },
                  "maxItems": 12
                }
              },
              "required": [
                "itemId",
                "products"
              ]
            },
            "minItems": 1,
            "maxItems": 200
          }
        },
        "required": [
          "items"
        ],
        "example": {
          "items": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001e",
              "products": [
                {
                  "id": "10450114",
                  "upc": null,
                  "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
                  "brand": "Great Value",
                  "size": "128 fl oz",
                  "image": "https://i5.walmartimages.com/seo/milk.jpeg",
                  "price": 327,
                  "salePrice": null,
                  "saleEnds": null,
                  "currency": "USD",
                  "aisle": null,
                  "inStock": true,
                  "url": "https://www.walmart.com/ip/10450114"
                }
              ]
            }
          ]
        }
      },
      "WalmartProduct": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[0-9A-Za-z]{1,64}$"
          },
          "upc": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 300
          },
          "brand": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "size": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000,
            "pattern": "^https:\\/\\/"
          },
          "price": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 10000000
          },
          "salePrice": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 10000000
          },
          "saleEnds": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Walmart's pages don't say, so the extension sends null."
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ]
          },
          "aisle": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60
          },
          "inStock": {
            "type": "boolean"
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000,
            "pattern": "^https:\\/\\/www\\.walmart\\.com\\/"
          }
        },
        "required": [
          "id",
          "upc",
          "name",
          "brand",
          "size",
          "image",
          "price",
          "salePrice",
          "saleEnds",
          "currency",
          "aisle",
          "inStock",
          "url"
        ],
        "example": {
          "id": "10450114",
          "upc": null,
          "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
          "brand": "Great Value",
          "size": "128 fl oz",
          "image": "https://i5.walmartimages.com/seo/milk.jpeg",
          "price": 327,
          "salePrice": null,
          "saleEnds": null,
          "currency": "USD",
          "aisle": null,
          "inStock": true,
          "url": "https://www.walmart.com/ip/10450114"
        }
      },
      "WalmartOptions": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StoreProduct"
            },
            "description": "What Walmart's search had for it when the extension last looked, Walmart's order with ads and what's out of stock last. Empty if it hasn't looked."
          }
        },
        "required": [
          "products"
        ],
        "example": {
          "products": [
            {
              "id": "10450114",
              "upc": null,
              "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
              "brand": "Great Value",
              "size": "128 fl oz",
              "image": "https://i5.walmartimages.com/seo/milk.jpeg",
              "price": 327,
              "salePrice": null,
              "saleEnds": null,
              "currency": "USD",
              "aisle": null,
              "inStock": true,
              "url": "https://www.walmart.com/ip/10450114"
            }
          ]
        }
      },
      "WalmartPick": {
        "type": "object",
        "properties": {
          "product": {
            "$ref": "#/components/schemas/WalmartProduct"
          }
        },
        "required": [
          "product"
        ],
        "example": {
          "product": {
            "id": "10450114",
            "upc": null,
            "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
            "brand": "Great Value",
            "size": "128 fl oz",
            "image": "https://i5.walmartimages.com/seo/milk.jpeg",
            "price": 327,
            "salePrice": null,
            "saleEnds": null,
            "currency": "USD",
            "aisle": null,
            "inStock": true,
            "url": "https://www.walmart.com/ip/10450114"
          }
        }
      },
      "WalmartSent": {
        "type": "object",
        "properties": {
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "itemId": {
                  "type": "string"
                },
                "packages": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 99
                },
                "product": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "pattern": "^[0-9A-Za-z]{1,64}$"
                    },
                    "upc": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 20
                    },
                    "name": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 300
                    },
                    "brand": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 120
                    },
                    "size": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 60
                    },
                    "image": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 1000,
                      "pattern": "^https:\\/\\/"
                    },
                    "price": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "minimum": 0,
                      "maximum": 10000000
                    },
                    "salePrice": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "minimum": 0,
                      "maximum": 10000000
                    },
                    "saleEnds": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date",
                      "description": "Walmart's pages don't say, so the extension sends null."
                    },
                    "currency": {
                      "type": "string",
                      "enum": [
                        "USD"
                      ]
                    },
                    "aisle": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 60
                    },
                    "inStock": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "maxLength": 1000,
                      "pattern": "^https:\\/\\/www\\.walmart\\.com\\/"
                    }
                  },
                  "required": [
                    "id",
                    "upc",
                    "name",
                    "brand",
                    "size",
                    "image",
                    "price",
                    "salePrice",
                    "saleEnds",
                    "currency",
                    "aisle",
                    "inStock",
                    "url"
                  ],
                  "example": {
                    "id": "10450114",
                    "upc": null,
                    "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
                    "brand": "Great Value",
                    "size": "128 fl oz",
                    "image": "https://i5.walmartimages.com/seo/milk.jpeg",
                    "price": 327,
                    "salePrice": null,
                    "saleEnds": null,
                    "currency": "USD",
                    "aisle": null,
                    "inStock": true,
                    "url": "https://www.walmart.com/ip/10450114"
                  },
                  "description": "The product as its page showed it when it was added."
                }
              },
              "required": [
                "itemId",
                "packages"
              ]
            },
            "minItems": 1,
            "maxItems": 300
          }
        },
        "required": [
          "lines"
        ],
        "example": {
          "lines": [
            {
              "itemId": "0199e8b0-5a10-7a01-8b00-00000000001d",
              "packages": 1,
              "product": {
                "id": "10450114",
                "upc": null,
                "name": "Great Value 2% Reduced Fat Milk, Gallon, 128 fl oz",
                "brand": "Great Value",
                "size": "128 fl oz",
                "image": "https://i5.walmartimages.com/seo/milk.jpeg",
                "price": 327,
                "salePrice": null,
                "saleEnds": null,
                "currency": "USD",
                "aisle": null,
                "inStock": true,
                "url": "https://www.walmart.com/ip/10450114"
              }
            }
          ]
        }
      },
      "TelemetryReport": {
        "type": "object",
        "properties": {
          "installId": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "install": {
            "type": "string"
          },
          "arch": {
            "type": "string"
          },
          "database": {
            "type": "string",
            "example": "postgres 18"
          },
          "modules": {
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "households": {
            "type": "string",
            "example": "2-5"
          },
          "people": {
            "type": "string"
          },
          "recipes": {
            "type": "string"
          }
        },
        "required": [
          "installId",
          "version",
          "install",
          "arch",
          "database",
          "modules",
          "households",
          "people",
          "recipes"
        ],
        "example": {
          "installId": "0199e8b0-5a10-7a01-8b00-0000000000ff",
          "version": "0.1.0",
          "install": "docker",
          "arch": "x64",
          "database": "sqlite",
          "modules": {
            "errorTracking": true,
            "telemetry": true
          },
          "households": "1",
          "people": "2-5",
          "recipes": "100-499"
        }
      },
      "HouseholdVoice": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "device",
              "kokoro",
              "openai"
            ],
            "description": "Who the server admin picked to read aloud."
          },
          "on": {
            "type": "boolean",
            "description": "Whether the server reads aloud for this household: false means each device reads in its own voice."
          }
        },
        "required": [
          "provider",
          "on"
        ],
        "example": {
          "provider": "kokoro",
          "on": true
        }
      },
      "Speak": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          }
        },
        "required": [
          "text"
        ],
        "example": {
          "text": "Bring a large pot of salted water to the boil."
        }
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "emailVerified": {
            "type": "boolean",
            "default": false,
            "readOnly": true
          },
          "image": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "username": {
            "type": "string"
          },
          "displayUsername": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "readOnly": true
          },
          "banned": {
            "type": "boolean",
            "default": false,
            "readOnly": true
          },
          "banReason": {
            "type": "string",
            "readOnly": true
          },
          "banExpires": {
            "type": "string",
            "format": "date-time",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "name",
          "email",
          "emailVerified",
          "createdAt",
          "updatedAt"
        ]
      },
      "Session": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "token": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "ipAddress": {
            "type": "string"
          },
          "userAgent": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "impersonatedBy": {
            "type": "string",
            "readOnly": true
          },
          "activeOrganizationId": {
            "type": "string",
            "readOnly": true
          }
        },
        "required": [
          "id",
          "expiresAt",
          "token",
          "createdAt",
          "updatedAt",
          "userId"
        ]
      },
      "Organization": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "logo": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "metadata": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "createdAt"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/api/assistant/chats": {
      "get": {
        "operationId": "listChats",
        "tags": [
          "Assistant"
        ],
        "summary": "Your chats",
        "description": "Your chats with the assistant in the household you're in, newest first. Nobody else's: each person's are their own. With an API key, it needs the `assistant: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:read",
        "responses": {
          "200": {
            "description": "Your chats.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startChat",
        "tags": [
          "Assistant"
        ],
        "summary": "Start a chat",
        "description": "A new chat with the assistant, to send messages to at `POST /assistant/chats/{chatId}/messages`. Its first message names it. With an API key, it needs the `assistant: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:write",
        "responses": {
          "201": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Chat"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/assistant/chats/{chatId}": {
      "get": {
        "operationId": "getChat",
        "tags": [
          "Assistant"
        ],
        "summary": "A chat, with its messages",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002b"
            },
            "required": true,
            "name": "chatId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The chat.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatWithMessages"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such chat of yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `assistant: read` permission."
      },
      "delete": {
        "operationId": "deleteChat",
        "tags": [
          "Assistant"
        ],
        "summary": "Delete a chat",
        "description": "Gone for good, with its messages and the files in them. What was applied from it stays. With an API key, it needs the `assistant: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002b"
            },
            "required": true,
            "name": "chatId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such chat of yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/assistant/chats/{chatId}/messages": {
      "post": {
        "operationId": "sendChatMessage",
        "tags": [
          "Assistant"
        ],
        "summary": "Ask the assistant",
        "description": "Sends a message, and streams the assistant's answer back as AI SDK's UI message stream (server-sent events), the way `useChat` reads it: its text, and the tools it uses as it uses them. It can see photos and PDFs sent with a message (uploaded first, with `POST /assistant/files`) when the model reads them, drafting a recipe from one, look things up in the household's recipes, plan, list and pantry, search the web when the model can, and propose changes, which come back as suggestions (`GET /proposals/{proposalId}`) for the person to apply. Both messages are kept. Uses the household's model for the assistant, which must be able to use tools, and counts towards its AI spend. With an API key, it needs the `assistant: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002b"
            },
            "required": true,
            "name": "chatId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChatMessageInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The answer, as it comes.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string",
                  "description": "AI SDK's UI message stream."
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such chat of yours, or no such file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The model can't use tools, or read photos or PDFs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/assistant/files": {
      "post": {
        "operationId": "uploadAssistantFile",
        "tags": [
          "Assistant"
        ],
        "summary": "Add a photo or PDF for the assistant",
        "description": "A file to send with a message, by its id in the message's `files`: a photo of what's in the pan, or a recipe to draft, as a photo or PDF. A photo is JPEG, PNG or WebP up to 15 MB, kept smaller and the right way up; a PDF is up to 10 MB. They're kept with the person's chats. With an API key, it needs the `assistant: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:write",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantFile"
                }
              }
            }
          },
          "400": {
            "description": "No file, or not a photo or PDF Merran can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/assistant/files/{fileId}": {
      "get": {
        "operationId": "getAssistantFile",
        "tags": [
          "Assistant"
        ],
        "summary": "A file sent to the assistant",
        "description": "A photo as JPEG, or a PDF. Only the person who sent it sees it, like their chats. A file never changes, so it can be kept for good. With an API key, it needs the `assistant: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "4f1c2a9e7b3d5f6a8c0e1b2d"
            },
            "required": true,
            "name": "fileId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The file.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such file of yours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteAssistantFile",
        "tags": [
          "Assistant"
        ],
        "summary": "Delete a file sent to the assistant",
        "description": "For one taken out of a message before it's sent. A message it's already in says it's gone. One that's already gone is fine. With an API key, it needs the `assistant: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "assistant:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "4f1c2a9e7b3d5f6a8c0e1b2d"
            },
            "required": true,
            "name": "fileId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/api-keys": {
      "get": {
        "operationId": "listApiKeys",
        "tags": [
          "API keys"
        ],
        "summary": "Your API keys",
        "description": "Your keys in every household you're in, newest first. The keys themselves can't be shown again.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKey"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createApiKey",
        "tags": [
          "API keys"
        ],
        "summary": "Make an API key",
        "description": "For the household in use, or another of yours named in `household`. It acts as you there, with only the permissions given, and stops working if you leave. The key is shown only in this response. Making a key needs a sign-in: a key can't make another.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made. Copy the key now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewApiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/api-keys/current": {
      "get": {
        "operationId": "checkApiKey",
        "tags": [
          "API keys"
        ],
        "summary": "Check an API key",
        "description": "The key this request was sent with: its household and what it may do. A quick way for a tool like Home Assistant to check its key works. Any API key that works will do: it needs no permission.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "any",
        "responses": {
          "200": {
            "description": "The key works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          },
          "400": {
            "description": "No API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key doesn't work.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/api-keys/{keyId}": {
      "delete": {
        "operationId": "revokeApiKey",
        "tags": [
          "API keys"
        ],
        "summary": "Revoke an API key",
        "description": "It stops working straight away.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "You have no such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/join/{token}": {
      "get": {
        "operationId": "getInvite",
        "tags": [
          "Sign-in"
        ],
        "summary": "See an invite",
        "description": "Which household an invite link is for, before joining.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The invite can be used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invite"
                }
              }
            }
          },
          "404": {
            "description": "No invite has that link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The link has been used, or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "joinHousehold",
        "tags": [
          "Sign-in"
        ],
        "summary": "Join a household",
        "description": "Signed in, this adds you to the invite's household. Signed out, send `account` to make one, which then signs in.",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "token",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JoinRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined, with an account you already had.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "201": {
            "description": "Joined, with a new account, and signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "description": "Signed out, and no account was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No invite has that link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already in the household, or the account exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The link has been used, or has expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/me": {
      "get": {
        "operationId": "getMe",
        "tags": [
          "Sign-in"
        ],
        "summary": "Who's signed in",
        "description": "The signed-in person, their households and the one in use on this device. Switch households with `POST /api/auth/organization/set-active`.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/me/export": {
      "get": {
        "operationId": "exportMyData",
        "tags": [
          "Auth"
        ],
        "summary": "Export your data",
        "description": "Your account, the households you're in, the cooking notes you wrote, and your API keys (never the keys themselves), as a JSON file to keep. A household's recipes are in the household's export.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your data, to download.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyData"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/setup": {
      "get": {
        "operationId": "getSetupStatus",
        "tags": [
          "Sign-in"
        ],
        "summary": "Whether the server still needs setting up",
        "responses": {
          "200": {
            "description": "True until the first account exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetupStatus"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "setUpServer",
        "tags": [
          "Sign-in"
        ],
        "summary": "Set up the server",
        "description": "Makes the first account, which is the server admin, and its household, then signs in. Needs the setup code printed in the server's log.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Set up, and signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                }
              }
            }
          },
          "400": {
            "description": "Something's wrong with what was sent, or the code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The server's already set up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many wrong codes. Try again in a few minutes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/capabilities": {
      "get": {
        "operationId": "getCapabilities",
        "tags": [
          "Server"
        ],
        "summary": "What's on",
        "description": "Which of Merran's optional parts are on, what they make possible, and what the web app needs from them. Needs no sign-in: the web app reads it before anyone signs in.",
        "responses": {
          "200": {
            "description": "What's on.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Capabilities"
                }
              }
            }
          }
        }
      }
    },
    "/api/collections": {
      "get": {
        "operationId": "listCollections",
        "tags": [
          "Collections"
        ],
        "summary": "Starter collections",
        "description": "Sets of recipes to start a library with, each filled from the recipe sites Merran reads by its own rule, around a few hand-picked classics. Only collections in season for the household, with at least four recipes that suit it. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "The collections.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CollectionList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/collections/{collection}": {
      "get": {
        "operationId": "getCollection",
        "tags": [
          "Collections"
        ],
        "summary": "A starter collection",
        "description": "Its recipes that suit the household, and what Find more found for it last. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "weeknight"
            },
            "required": true,
            "name": "collection",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Collection"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/collections/{collection}/import": {
      "post": {
        "operationId": "importCollection",
        "tags": [
          "Collections"
        ],
        "summary": "Import a whole collection",
        "description": "Its recipes not in the library yet, in the background, as several links are: follow along at /recipes/import/links/{importId}. With an API key, it needs the `imports: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "weeknight"
            },
            "required": true,
            "name": "collection",
            "in": "path"
          }
        ],
        "responses": {
          "202": {
            "description": "Importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkImport"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Every recipe in it is in the library already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/collections/{collection}/find": {
      "post": {
        "operationId": "findMoreForCollection",
        "tags": [
          "Collections"
        ],
        "summary": "Find more for a collection",
        "description": "Searches the web for more recipes in the collection's theme that suit the household, as `POST /discover` does, and keeps them with the collection (its `found`). Needs a model that can search the web. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "weeknight"
            },
            "required": true,
            "name": "collection",
            "in": "path"
          }
        ],
        "responses": {
          "202": {
            "description": "Searching.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Discovery"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such collection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A search is going already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/discover": {
      "get": {
        "operationId": "listDiscoveries",
        "tags": [
          "Discover"
        ],
        "summary": "Recent searches",
        "description": "The household's last twelve searches, newest first. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "Its searches.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscoveryList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startDiscovery",
        "tags": [
          "Discover"
        ],
        "summary": "Find new recipes online",
        "description": "Searches the web for recipes that fit a theme (\"cozy fall soups\") and suit the household, in the background, with the household's model for finding recipes, which must be able to search the web. Merran opens each page and keeps those it can import. Follow it at `GET /discover/{discoveryId}`; one at a time for a household. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DiscoverInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Searching.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Discovery"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A search is going already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/discover/{discoveryId}": {
      "get": {
        "operationId": "getDiscovery",
        "tags": [
          "Discover"
        ],
        "summary": "A search",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002d"
            },
            "required": true,
            "name": "discoveryId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The search, as far as it's got.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Discovery"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such search.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `recipes: read` permission."
      }
    },
    "/api/events": {
      "get": {
        "operationId": "streamEvents",
        "tags": [
          "Live"
        ],
        "summary": "What changes, as it happens",
        "description": "Server-sent events for the household, for as long as the connection stays open:\n\n- `ready` when it's open, with the areas it hears about.\n- `plan`, `list`, `pantry`, `recipes` or `household` when something there changes, with who changed it (`by`). Fetch the area again to see what.\n- `presence` with who has `page` open, when someone opens or leaves it.\n- `ping` now and then, to keep the connection open.\n\nA device that sends the same `client` with its own changes (in the `X-Merran-Client` header) isn't told about them; your other devices are. An API key hears about the areas it can read. With an API key, it needs the `any: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "any:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "list",
                "plan"
              ],
              "description": "The page open, to say who else has it open."
            },
            "required": false,
            "description": "The page open, to say who else has it open.",
            "name": "page",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 64,
              "description": "This device's id, as sent with its changes."
            },
            "required": false,
            "description": "This device's id, as sent with its changes.",
            "name": "client",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The events, until the connection closes.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "checkHealth",
        "tags": [
          "Server"
        ],
        "summary": "Check the server is up",
        "description": "Answers as long as the server is running. Needs no sign-in, and says nothing personal.",
        "responses": {
          "200": {
            "description": "The server is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/api/health/ready": {
      "get": {
        "operationId": "checkReady",
        "tags": [
          "Server"
        ],
        "summary": "Check the server can do its work",
        "description": "Checks the database answers and is up to date. For uptime monitors. Needs no sign-in.",
        "responses": {
          "200": {
            "description": "Ready.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ready"
                }
              }
            }
          },
          "503": {
            "description": "Not ready: the database is unreachable or out of date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households": {
      "post": {
        "operationId": "createHousehold",
        "tags": [
          "Households"
        ],
        "summary": "Start another household",
        "description": "Server admins only, for now. You become its owner, and it becomes the one in use on this device.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  }
                },
                "required": [
                  "name"
                ],
                "example": {
                  "name": "Grandma's kitchen"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Household"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current": {
      "get": {
        "operationId": "getHousehold",
        "tags": [
          "Households"
        ],
        "summary": "The household in use",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "household:read",
        "responses": {
          "200": {
            "description": "The household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Household"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `household: read` permission."
      },
      "patch": {
        "operationId": "updateHousehold",
        "tags": [
          "Households"
        ],
        "summary": "Change the household",
        "description": "Household admins only.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseholdChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Household"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/ai": {
      "get": {
        "operationId": "getHouseholdAi",
        "tags": [
          "AI"
        ],
        "summary": "The household's AI",
        "description": "Anyone in the household. For each AI task, the model and whose key pays, the household's or the server's, or why there's none; this month's spend against the limits; its own keys and addresses (only their last four characters); the models it can pick; and what it's spent by month, task and person.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The household's AI.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdAi"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "saveHouseholdAi",
        "tags": [
          "AI"
        ],
        "summary": "Pick the household's AI models",
        "description": "Household admins only. Its models for big and small tasks, and for any task of its own; null goes by its keys, or the server's. And a monthly limit on its own keys.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HouseholdAiSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdAi"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/ai/keys": {
      "post": {
        "operationId": "addHouseholdAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Add an AI key or address for the household",
        "description": "Household admins only, when the server admin lets households add their own: keys, and separately, OpenAI-compatible addresses, which must be public. Only this household uses them. A provider's key replaces its one before. It's tested against the provider first, at no cost, and kept sealed.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewAiKey"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tested and kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The provider couldn't be reached, or answered with an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/ai/keys/{keyId}": {
      "delete": {
        "operationId": "removeHouseholdAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Take off one of the household's AI keys",
        "description": "Household admins only.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Taken off."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/ai/keys/{keyId}/credit": {
      "put": {
        "operationId": "setHouseholdAiKeyCredit",
        "tags": [
          "AI"
        ],
        "summary": "Note an AI key's credit",
        "description": "Household admins only. The credit on the key now, as its provider's dashboard says: the providers don't tell a key, so Merran counts what it spends from here and warns when about a fifth is left. Entering it also tries the key again if it ran out. Null stops counting.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCredit"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Noted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/ai/keys/{keyId}/test": {
      "post": {
        "operationId": "testHouseholdAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Test one of the household's AI keys",
        "description": "Household admins only. Asks the provider what the key can use, which costs nothing, and notes it.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "It works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The provider couldn't be reached, or answered with an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/export": {
      "get": {
        "operationId": "exportHousehold",
        "tags": [
          "Households"
        ],
        "summary": "Export the household",
        "description": "Household admins only. A zip of the household's details and recipes, with their photos, versions, cooking notes and pairings, in the Merran export format (docs/export-format.md). Recipes in Recently deleted, secrets, API keys and invites stay behind.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The export, as a zip.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/import": {
      "post": {
        "operationId": "importIntoHousehold",
        "tags": [
          "Households"
        ],
        "summary": "Import a Merran export into the household",
        "description": "Household admins only. The body is the export's zip. It's checked, then imported in the background: follow the job at /households/current/jobs/{jobId}. Recipes already here, like from importing the same export twice, are skipped.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "400": {
            "description": "That isn't a Merran export this Merran can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/import/files": {
      "post": {
        "operationId": "checkImportFile",
        "tags": [
          "Households"
        ],
        "summary": "Check a file from another app",
        "description": "Household admins only. The body is the file: a Mealie backup, or recipes exported from Mealie. It's kept, and what's in it is said, so its import can be started at /households/current/import/files/{fileId}/start. A file not started is cleared away after a week.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What's in it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportFile"
                }
              }
            }
          },
          "400": {
            "description": "Merran can't read that file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/import/files/{fileId}/start": {
      "post": {
        "operationId": "startFileImport",
        "tags": [
          "Households"
        ],
        "summary": "Import a checked file from another app",
        "description": "Household admins only. Imports a file checked at /households/current/import/files, in the background: follow the job at /households/current/jobs/{jobId}. Recipes an earlier import brought in are skipped.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "fileId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartImportFile"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The file isn't here any more.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/import/mealie/check": {
      "post": {
        "operationId": "checkMealie",
        "tags": [
          "Households"
        ],
        "summary": "Connect to a Mealie",
        "description": "The server admin only, since Mealie is usually on the home network and this reaches the address given. Says which Mealie it is and how many recipes it has. The token isn't kept.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MealieConnection"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Connected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MealieCheck"
                }
              }
            }
          },
          "400": {
            "description": "Mealie didn't take the token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Couldn't reach Mealie, or it isn't Mealie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/import/mealie": {
      "post": {
        "operationId": "importFromMealie",
        "tags": [
          "Households"
        ],
        "summary": "Import from a Mealie",
        "description": "The server admin only. Imports every recipe in the Mealie, in the background: follow the job at /households/current/jobs/{jobId}. Recipes an earlier import brought in are skipped. The token is sealed while the import runs, and forgotten once it's finished.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MealieConnection"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "400": {
            "description": "Mealie didn't take the token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Couldn't reach Mealie, or it isn't Mealie.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/invites": {
      "get": {
        "operationId": "listInvites",
        "tags": [
          "Households"
        ],
        "summary": "Invite links that can still be used",
        "description": "Household admins only. The links themselves can't be shown again.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Unused links, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InviteLink"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createInvite",
        "tags": [
          "Households"
        ],
        "summary": "Make an invite link",
        "description": "Household admins only. The link works once, for 7 days. Send it to the person; it's shown only now.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "member"
                    ],
                    "default": "member"
                  }
                },
                "example": {
                  "role": "member"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NewInviteLink"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/invites/{inviteId}": {
      "delete": {
        "operationId": "cancelInvite",
        "tags": [
          "Households"
        ],
        "summary": "Cancel an invite link",
        "description": "Household admins only.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "inviteId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such invite.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/jobs/{jobId}": {
      "get": {
        "operationId": "getHouseholdJob",
        "tags": [
          "Households"
        ],
        "summary": "One of the household's jobs",
        "description": "Household admins only. How a job for this household, like an import, is getting on.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "jobId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/jobs/{jobId}/cancel": {
      "post": {
        "operationId": "cancelHouseholdJob",
        "tags": [
          "Households"
        ],
        "summary": "Stop one of the household's jobs",
        "description": "Household admins only. What it already did stays: an import keeps the recipes it brought in.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "jobId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/members": {
      "get": {
        "operationId": "listMembers",
        "tags": [
          "Households"
        ],
        "summary": "Who's in the household",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "household:read",
        "responses": {
          "200": {
            "description": "Its people, longest-standing first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Member"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `household: read` permission."
      }
    },
    "/api/households/current/members/{memberId}": {
      "patch": {
        "operationId": "updateMember",
        "tags": [
          "Households"
        ],
        "summary": "Change someone's role",
        "description": "Household admins only. Only owners can make or unmake owners, and there's always at least one owner.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "memberId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "owner",
                      "admin",
                      "member"
                    ]
                  }
                },
                "required": [
                  "role"
                ],
                "example": {
                  "role": "admin"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Member"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such person in the household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That would leave no owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removeMember",
        "tags": [
          "Households"
        ],
        "summary": "Remove someone, or leave",
        "description": "Household admins can remove people; anyone can remove themselves. The last owner can't leave.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "memberId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such person in the household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That would leave no owner.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/on-hand": {
      "get": {
        "operationId": "getOnHand",
        "tags": [
          "Households"
        ],
        "summary": "What the household always has",
        "description": "Left off lists made from the plan: salt, pepper, water and ice to start. Each covers its kinds, so salt is kosher and sea salt too. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "What's always on hand.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnHand"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "setOnHand",
        "tags": [
          "Households"
        ],
        "summary": "Change what the household always has",
        "description": "Anyone in the household can. Saved tidied: lowercase, without repeats, and \"salt and pepper\" as salt and pepper. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OnHand"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnHand"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/tastes": {
      "get": {
        "operationId": "getTastes",
        "tags": [
          "Households"
        ],
        "summary": "What the household eats",
        "description": "Foods it avoids (an allergy is never suggested), diets every meal must fit, and how long a weeknight dinner can take. Fill the week keeps to them. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "responses": {
          "200": {
            "description": "Its tastes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tastes"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "setTastes",
        "tags": [
          "Households"
        ],
        "summary": "Change what the household eats",
        "description": "Anyone in the household can: everyone eats. Replaces them all; a food listed twice is kept once, as an allergy if either says so. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Tastes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Tastes"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/current/themes": {
      "get": {
        "operationId": "getThemeNights",
        "tags": [
          "Households"
        ],
        "summary": "The household's theme nights",
        "description": "Dinner's theme each weekday, Sunday first: taco night, fish on Friday, eat out on Saturday. Fill the week keeps to them. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "responses": {
          "200": {
            "description": "Its theme nights.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThemeNights"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "setThemeNights",
        "tags": [
          "Households"
        ],
        "summary": "Change the household's theme nights",
        "description": "Household admins only. Replaces the whole week.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ThemeNights"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ThemeNights"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/households/import": {
      "post": {
        "operationId": "importHousehold",
        "tags": [
          "Households"
        ],
        "summary": "Import a Merran export as a new household",
        "description": "Server admins only, as for starting any household. The body is the export's zip. A household is made with the export's name and details, you become its owner, and it becomes the one in use on this device; its recipes are imported in the background: follow the job at /households/current/jobs/{jobId}.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Made, and importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdImport"
                }
              }
            }
          },
          "400": {
            "description": "That isn't a Merran export this Merran can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list": {
      "get": {
        "operationId": "getList",
        "tags": [
          "List"
        ],
        "summary": "The shopping lists",
        "description": "The household's lists (one for each store it shops at, the main one first), and everything to buy, by aisle in a walk through the store, with what's checked off. Each item says which list it's on, how much, and which planned meals it's for. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "The list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShoppingList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/clear": {
      "post": {
        "operationId": "clearList",
        "tags": [
          "List"
        ],
        "summary": "Clear the list",
        "description": "Takes off what's checked, or everything. The answer says what was there, for Undo with /list/restore. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ClearList"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cleared.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListBefore"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/every-week": {
      "get": {
        "operationId": "listEveryWeek",
        "tags": [
          "List"
        ],
        "summary": "What's bought every week",
        "description": "Bought whatever's planned (bananas, milk). Adding from the plan puts those not on the list on it. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "The every-week items.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EveryWeek"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "addEveryWeekItem",
        "tags": [
          "List"
        ],
        "summary": "Buy something every week",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewEveryWeekItem"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Bought every week now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EveryWeekItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `list: write` permission."
      }
    },
    "/api/list/every-week/{weeklyId}": {
      "patch": {
        "operationId": "updateEveryWeekItem",
        "tags": [
          "List"
        ],
        "summary": "Change something bought every week",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000023"
            },
            "required": true,
            "name": "weeklyId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EveryWeekChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EveryWeekItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not bought every week.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": "With an API key, it needs the `list: write` permission."
      },
      "delete": {
        "operationId": "removeEveryWeekItem",
        "tags": [
          "List"
        ],
        "summary": "Stop buying something every week",
        "description": "What's on the list now stays. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000023"
            },
            "required": true,
            "name": "weeklyId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Not bought every week any more."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not bought every week.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/foods": {
      "get": {
        "operationId": "listFoodAliases",
        "tags": [
          "List"
        ],
        "summary": "The household's own names for foods",
        "description": "What it said was the same food as another (Same as…) or kept apart (Not the same) on the list, which win over Merran's own names for it. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "Its names, by name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FoodAliases"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/foods/{aliasId}": {
      "delete": {
        "operationId": "forgetFoodAlias",
        "tags": [
          "List"
        ],
        "summary": "Forget one of the household's names for a food",
        "description": "Merran's own name for it counts again. Lines already joined stay as they are. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001f"
            },
            "required": true,
            "name": "aliasId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Forgotten.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ],
                  "example": {
                    "ok": true
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/from-plan": {
      "get": {
        "operationId": "previewFromPlan",
        "tags": [
          "List"
        ],
        "summary": "What adding from the plan would add",
        "description": "Planned meals from one day to another that aren't on the list yet, and how many are: the rest of this week if no days are given. Meals with a recipe only; notes aren't shopped for. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-10-15"
            },
            "required": false,
            "name": "from",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-10-15"
            },
            "required": false,
            "name": "to",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The meals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FromPlanPreview"
                }
              }
            }
          },
          "400": {
            "description": "Days out of order, or too many at once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "addFromPlan",
        "tags": [
          "List"
        ],
        "summary": "Add planned meals to the list",
        "description": "The ingredients of planned meals from one day to another that aren't on the list yet: scaled to their servings, their sides' too, added up across meals and into what's there, leaving off what's always on hand. Every-week items not on the list come along. From then on the list follows the plan: taking a meal off, skipping it or changing it takes its share back off. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FromPlan"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FromPlanResult"
                }
              }
            }
          },
          "400": {
            "description": "Days out of order, or too many at once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items": {
      "post": {
        "operationId": "addListItem",
        "tags": [
          "List"
        ],
        "summary": "Put something on the list",
        "description": "As typed: \"2 lb ground beef\" is ground beef, 2 lb; \"paper towels\" is just that. It goes in the aisle and on the list the household last put the food in, else a first guess and the list for that aisle. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewListItem"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "On the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such aisle or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items/bulk": {
      "post": {
        "operationId": "changeListItems",
        "tags": [
          "List"
        ],
        "summary": "Change several things on the list at once",
        "description": "Take them off, check them off or not, move them to an aisle or another list (remembered for each food), or buy them every week: all or none. The answer says what they were before, for Undo with /list/restore. Ids not on the list are passed over. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListBulk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListBulkResult"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such aisle or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items/{itemId}": {
      "patch": {
        "operationId": "updateListItem",
        "tags": [
          "List"
        ],
        "summary": "Change something on the list",
        "description": "Check it off or not, rename it, change how much, or move it to another aisle or list, which the household remembers for the food. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListItemChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list, or no such aisle or list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removeListItem",
        "tags": [
          "List"
        ],
        "summary": "Take something off the list",
        "description": "The answer is what it was, for Undo with /list/restore. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Taken off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListItemSnapshot"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items/{itemId}/apart": {
      "post": {
        "operationId": "keepListItemNamesApart",
        "tags": [
          "List"
        ],
        "summary": "Say names a line was joined from aren't the same",
        "description": "Each name is its own food for the household from the next list on (docs/foods.md). The line as it is stays. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeepApart"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remembered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KeptApart"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items/{itemId}/names": {
      "get": {
        "operationId": "listItemNames",
        "tags": [
          "List"
        ],
        "summary": "What a line on the list was joined from",
        "description": "Its own name, and the names in its meals' recipes that came to the same food: what Not the same can keep apart. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The names.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JoinedNames"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/items/{itemId}/same-as": {
      "post": {
        "operationId": "sameAsListItem",
        "tags": [
          "List"
        ],
        "summary": "Say something on the list is the same food as another",
        "description": "The household's own name for the food (docs/foods.md): from now on it's the other's food, on the list, at home and in what's to come, and the two lines join where their amounts add up. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SameAs"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remembered, and joined.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SameAsResult"
                }
              }
            }
          },
          "400": {
            "description": "The same thing twice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/lists": {
      "put": {
        "operationId": "saveLists",
        "tags": [
          "List"
        ],
        "summary": "Change the household's lists",
        "description": "Household admins only. The lists as a whole, in order: one with no id is new, and one left out goes, with what's on it moving to the main list, which has to stay. Each store has one list, and each aisle goes on one. Then what's waiting on the main list moves to the list its food or aisle goes on now.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lists"
                }
              }
            }
          },
          "400": {
            "description": "The main list left out, too many lists, a store twice or an aisle on two lists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such list or aisle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/put-away": {
      "post": {
        "operationId": "putAway",
        "tags": [
          "List"
        ],
        "summary": "Put the shopping away",
        "description": "What's checked off (on a list, or all of them) goes in the pantry: set aside for the meals it was bought for, in the fridge, freezer or cupboard, with a use-by guess for fresh food. Household things and what's always on hand are cleared without going in. Everything bought goes in the purchase history. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutAway"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Put away.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PutAwayResult"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/restore": {
      "post": {
        "operationId": "restoreListItems",
        "tags": [
          "List"
        ],
        "summary": "Put things on the list back as they were",
        "description": "Undo: items as taking them off or changing them said they were. One taken off comes back with its id. An aisle that's gone is left empty. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListRestore"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Back as they were.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListItems"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/usual": {
      "get": {
        "operationId": "listUsualBuys",
        "tags": [
          "List"
        ],
        "summary": "Usual buys due",
        "description": "What the household gets every so often, learned from the shopping it's put away (bought on three days or more), now due again: \"Milk? You last got some Oct 9.\" Not what's on the list, at home, bought every week or always on hand, nor what's been passed over until it's due again. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "Due now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsualBuys"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/usual/add": {
      "post": {
        "operationId": "addUsualBuys",
        "tags": [
          "List"
        ],
        "summary": "Put usual buys on the list",
        "description": "As they were last bought, each in its aisle and on its list. Ones not due are passed over. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsualAdd"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "On the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListItems"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/list/usual/skip": {
      "post": {
        "operationId": "skipUsualBuy",
        "tags": [
          "List"
        ],
        "summary": "Not this time",
        "description": "A usual buy isn't offered again until it's next due. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsualSkip"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Passed over."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry": {
      "get": {
        "operationId": "getPantry",
        "tags": [
          "Pantry"
        ],
        "summary": "What's at home",
        "description": "The fridge, the freezer and the cupboard: how much of each thing, its use-by date, the planned meals it's set aside for, and why to use it soon when it should be. With an API key, it needs the `pantry: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:read",
        "responses": {
          "200": {
            "description": "What's at home.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pantry"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/clear": {
      "post": {
        "operationId": "clearPantry",
        "tags": [
          "Pantry"
        ],
        "summary": "Empty the pantry",
        "description": "Everything out. The answer says what was there, for Undo with /pantry/restore. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "responses": {
          "200": {
            "description": "Empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantryBefore"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/items": {
      "post": {
        "operationId": "addPantryItem",
        "tags": [
          "Pantry"
        ],
        "summary": "Put something in the pantry",
        "description": "As typed: \"2 lb chicken thighs\" or just \"rice\". It goes in the fridge, freezer or cupboard as the food suggests, with a use-by guess for fresh food, and joins the same food already there when the amounts add up. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPantryItem"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "At home.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantryItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/items/bulk": {
      "post": {
        "operationId": "changePantryItems",
        "tags": [
          "Pantry"
        ],
        "summary": "Change several things at home at once",
        "description": "Used up, moved to the fridge, freezer or cupboard, or given a use-by date (null for none): all or none. The answer says what they were before, for Undo with /pantry/restore. Ids not in the pantry are passed over. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PantryBulk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantryBulkResult"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/items/{itemId}": {
      "delete": {
        "operationId": "removePantryItem",
        "tags": [
          "Pantry"
        ],
        "summary": "Used up, or thrown out",
        "description": "The answer is what it was, for Undo with /pantry/restore. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000026"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantrySnapshot"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not in the pantry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updatePantryItem",
        "tags": [
          "Pantry"
        ],
        "summary": "Change something at home",
        "description": "Rename it, change how much, where it's kept, its use-by date or its aisle. Moving it to the freezer takes its use-by date off, unless one's given. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000026"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PantryChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantryItem"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not in the pantry, or no such aisle.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/receipts": {
      "get": {
        "operationId": "listReceipts",
        "tags": [
          "Pantry"
        ],
        "summary": "Receipts waiting for a look",
        "description": "Scanned, shared or sent to the assistant, and not added or discarded yet, newest first. Each is forgotten after a day. With an API key, it needs the `pantry: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:read",
        "responses": {
          "200": {
            "description": "The receipts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiptList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "scanReceipt",
        "tags": [
          "Pantry"
        ],
        "summary": "Scan a receipt",
        "description": "Reads a receipt into a draft to look over before anything goes in the pantry: photos of a paper one, screenshots or a PDF of an emailed one (up to 6 files, one `file` field each), or its `text`. Each line says what it is, how much, and the shopping list item it was bought for. Add it with `POST /pantry/receipts/{receiptId}/add`; it's forgotten after a day. Uses the household's model for receipts, which must be able to read photos or PDFs if those are sent, and counts towards its AI spend. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "A photo, screenshot or PDF of the receipt."
                  },
                  "text": {
                    "type": "string",
                    "description": "The receipt's text, as from an email."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Read, waiting for a look.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "400": {
            "description": "Nothing to read, or a file Merran can't read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The files are too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "It isn't a receipt (`receipt_is_recipe` when it's a recipe), or the model can't read photos or PDFs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/receipts/{receiptId}": {
      "get": {
        "operationId": "getReceipt",
        "tags": [
          "Pantry"
        ],
        "summary": "A receipt waiting for a look",
        "description": "One scanned, shared or sent to the assistant, and not added or discarded yet. With an API key, it needs the `pantry: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002e"
            },
            "required": true,
            "name": "receiptId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Receipt"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such receipt waiting: added, discarded, or a day old.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "discardReceipt",
        "tags": [
          "Pantry"
        ],
        "summary": "Discard a receipt",
        "description": "Forgets it, with its photos, without adding anything. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002e"
            },
            "required": true,
            "name": "receiptId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Discarded."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such receipt waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/receipts/{receiptId}/add": {
      "post": {
        "operationId": "addReceipt",
        "tags": [
          "Pantry"
        ],
        "summary": "Put a receipt's lines in the pantry",
        "description": "The lines picked, named and measured as looked over, go in the pantry like shopping put away: in the fridge, freezer or cupboard with a use-by guess, kept for the meals the list item was for, and noted for usual buys. The list items they were bought for come off the list. The receipt's done with after. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002e"
            },
            "required": true,
            "name": "receiptId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddReceiptInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReceiptAdded"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such receipt waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/receipts/{receiptId}/files/{index}": {
      "get": {
        "operationId": "getReceiptFile",
        "tags": [
          "Pantry"
        ],
        "summary": "A receipt's photo or PDF",
        "description": "To compare with its lines while looking it over. With an API key, it needs the `pantry: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002e"
            },
            "required": true,
            "name": "receiptId",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "example": "0"
            },
            "required": true,
            "name": "index",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The photo, as JPEG, or the PDF.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such receipt waiting, or no such file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pantry/restore": {
      "post": {
        "operationId": "restorePantryItems",
        "tags": [
          "Pantry"
        ],
        "summary": "Put things at home back as they were",
        "description": "Undo: things as using them up or changing them said they were. One used up comes back with its id. An aisle that's gone is left empty. With an API key, it needs the `pantry: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "pantry:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PantryRestore"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Back as they were.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PantryItems"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan": {
      "get": {
        "operationId": "getPlan",
        "tags": [
          "Plan"
        ],
        "summary": "The plan for some days",
        "description": "Meals from one day to another, up to 62 days apart, in a day's order: this week's if no days are given. A week's pool (meals for any day of it) is dated to the week's first day, with `anyDay` set. Meals of recipes in Recently deleted are left out until they're restored. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-10-15",
              "description": "The first day. This week's first day if left out."
            },
            "required": false,
            "description": "The first day. This week's first day if left out.",
            "name": "from",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-10-15",
              "description": "The last day. Six days after `from` if left out."
            },
            "required": false,
            "description": "The last day. Six days after `from` if left out.",
            "name": "to",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The meals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "400": {
            "description": "Not days, or too many of them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/autoplan": {
      "get": {
        "operationId": "getAutoplan",
        "tags": [
          "Plan"
        ],
        "summary": "The weekly autoplan",
        "description": "When it runs (a day and hour in the household's time zone), the meals it fills, whether it puts them on the list, when it runs next and what it did last. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "responses": {
          "200": {
            "description": "The autoplan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Autoplan"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "saveAutoplan",
        "tags": [
          "Plan"
        ],
        "summary": "Change the weekly autoplan",
        "description": "Household admins only. On its day and hour it fills the week ahead's empty meals the way Fill the week does, saying why for each, and can put what they need on the list. Turning it on plans nothing straight away: it waits for its day and hour. A run that's late, with the server off, plans what's left of its week.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AutoplanSettings"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Autoplan"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/autoplan/run": {
      "post": {
        "operationId": "planAheadNow",
        "tags": [
          "Plan"
        ],
        "summary": "Plan the week ahead now",
        "description": "What the autoplan does, now: the next week starting today or later, its empty meals filled, and onto the list if the autoplan says so. That week counts as planned, so the autoplan won't plan it again. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "responses": {
          "200": {
            "description": "Planned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutoplanRun"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/next": {
      "get": {
        "operationId": "getWhatsNext",
        "tags": [
          "Plan"
        ],
        "summary": "What's next",
        "description": "The next meal still planned, by the household's clock: today's from the meal of the hour on (breakfast until 11, lunch until 3, then dinner), then the days after. Snacks and the week's pool aren't counted. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "responses": {
          "200": {
            "description": "What's next.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhatsNext"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/entries": {
      "post": {
        "operationId": "addPlanEntry",
        "tags": [
          "Plan"
        ],
        "summary": "Put a meal on the plan",
        "description": "A recipe or a note, on a day's meal, after any dishes already there. With `anyDay`, it goes in the pool of the week the date is in. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPlanEntry"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "On the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanEntry"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/entries/batch": {
      "post": {
        "operationId": "addPlanEntries",
        "tags": [
          "Plan"
        ],
        "summary": "Put several meals on the plan",
        "description": "Like putting each on, but all or none: what Fill the week adds. Each goes after any dishes already in its slot. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewPlanEntries"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "On the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanEntries"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/entries/bulk": {
      "post": {
        "operationId": "changePlanEntries",
        "tags": [
          "Plan"
        ],
        "summary": "Change several meals at once",
        "description": "Take them off, move them (to another day or meal, or the week's pool), or mark them planned, cooked or skipped: all or none. The answer says what they were before, for Undo with /plan/restore. Ids not on the plan are passed over. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanBulk"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanBulkResult"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/entries/{entryId}": {
      "patch": {
        "operationId": "changePlanEntry",
        "tags": [
          "Plan"
        ],
        "summary": "Change a meal",
        "description": "Move it (it goes after any dishes in its new slot), swap its recipe, give it a side, change its servings, or mark it planned, cooked or skipped. Cooked sets when its recipe and side were last made. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "entryId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanEntryChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanEntry"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such meal on the plan, or no such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "removePlanEntry",
        "tags": [
          "Plan"
        ],
        "summary": "Take a meal off the plan",
        "description": "The answer is the meal as it was, to put back with /plan/restore (Undo). With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "entryId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Taken off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanEntrySnapshot"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such meal on the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/repeat": {
      "post": {
        "operationId": "repeatLastWeek",
        "tags": [
          "Plan"
        ],
        "summary": "Repeat last week",
        "description": "The week before's meals, again: each on the same weekday and meal where that slot is still empty, and its pool into this week's pool. Skipped meals stay behind; the copies start as planned. The answer is what was added, for Undo (take them off in bulk). With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanWeek"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/clear": {
      "post": {
        "operationId": "clearPlan",
        "tags": [
          "Plan"
        ],
        "summary": "Clear the rest of the week",
        "description": "Takes off the meals still planned for the rest of the week (from today, in this week; all of it, in another) and its pool. Cooked and skipped meals stay. The answer is what went, to put back with /plan/restore. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanWeek"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Cleared.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanCleared"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/restore": {
      "post": {
        "operationId": "restorePlanEntries",
        "tags": [
          "Plan"
        ],
        "summary": "Put meals back as they were",
        "description": "Undo: meals as other changes said they were. Ones taken off come back with the same id; ones changed go back to how they were. A meal whose recipe is gone is passed over. With an API key, it needs the `plan: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlanRestore"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "As they were.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Plan"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/plan/suggest": {
      "post": {
        "operationId": "suggestMeals",
        "tags": [
          "Plan"
        ],
        "summary": "Suggest meals for open slots",
        "description": "A recipe for each slot, saying why: Fill the week and Something else. Keeps to the household's tastes and theme nights, doesn't repeat a dish within two weeks, and pairs a side when one goes with it. Nothing is planned; slots with nothing suitable are left out. Asking again gives other ideas. With an API key, it needs the `plan: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "plan:read",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SuggestRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ideas, in the slots' order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Suggestions"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/proposals": {
      "get": {
        "operationId": "listProposals",
        "tags": [
          "Review"
        ],
        "summary": "The Review queue",
        "description": "What AI suggests, waiting to be applied or dismissed, newest first, a page at a time. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0
            },
            "required": false,
            "name": "offset",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only this recipe's, by its id or address."
            },
            "required": false,
            "description": "Only this recipe's, by its id or address.",
            "name": "recipe",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "What's waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProposalList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/proposals/{proposalId}": {
      "get": {
        "operationId": "getProposal",
        "tags": [
          "Review"
        ],
        "summary": "A suggestion",
        "description": "Waiting, applied or dismissed. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000029"
            },
            "required": true,
            "name": "proposalId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The suggestion.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Proposal"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such suggestion.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/proposals/{proposalId}/apply": {
      "post": {
        "operationId": "applyProposal",
        "tags": [
          "Review"
        ],
        "summary": "Apply a suggestion",
        "description": "Changes to a recipe apply the sections picked, as one change that keeps a version of how it was; the rest are skipped. A section changed since it was suggested is refused rather than overwritten, and the error says which. The assistant's other suggestions add what they say: a recipe, meals, things for the list, tastes, or a recipe from the web. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000029"
            },
            "required": true,
            "name": "proposalId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApplyInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied: the suggestion and the recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplyResult"
                }
              }
            }
          },
          "400": {
            "description": "Nothing picked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such suggestion, or its recipe is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Applied or dismissed already, or the recipe changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/proposals/{proposalId}/dismiss": {
      "post": {
        "operationId": "dismissProposal",
        "tags": [
          "Review"
        ],
        "summary": "Dismiss a suggestion",
        "description": "The recipe stays as it is. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000029"
            },
            "required": true,
            "name": "proposalId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Dismissed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Proposal"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such suggestion.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Applied or dismissed already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes": {
      "get": {
        "operationId": "listRecipes",
        "tags": [
          "Recipes"
        ],
        "summary": "Find recipes",
        "description": "The household's recipes, a page at a time: searched, filtered by tags, and sorted. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 200,
              "description": "Words in the name, description or ingredients."
            },
            "required": false,
            "description": "Words in the name, description or ingredients.",
            "name": "q",
            "in": "query"
          },
          {
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ],
              "description": "Only recipes with this tag. Repeat it for all of several."
            },
            "required": false,
            "description": "Only recipes with this tag. Repeat it for all of several.",
            "name": "tag",
            "in": "query"
          },
          {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/Diet"
                },
                {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Diet"
                  }
                }
              ],
              "description": "Only recipes with this diet label. Repeat it for all of several."
            },
            "required": false,
            "description": "Only recipes with this diet label. Repeat it for all of several.",
            "name": "diet",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "description": "Only recipes ready in this many minutes or fewer."
            },
            "required": false,
            "description": "Only recipes ready in this many minutes or fewer.",
            "name": "maxMinutes",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only recipes not made yet (true), or only ones made (false)."
            },
            "required": false,
            "description": "Only recipes not made yet (true), or only ones made (false).",
            "name": "new",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Only recipes that use a real deal at the household's store (true), from `GET /stores/deals`."
            },
            "required": false,
            "description": "Only recipes that use a real deal at the household's store (true), from `GET /stores/deals`.",
            "name": "onSale",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "name",
                "updated",
                "rating",
                "made"
              ],
              "default": "newest",
              "description": "Newest added, name, recently changed, top rated, or recently made."
            },
            "required": false,
            "description": "Newest added, name, recently changed, top rated, or recently made.",
            "name": "sort",
            "in": "query"
          },
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 48
            },
            "required": false,
            "name": "limit",
            "in": "query"
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0
            },
            "required": false,
            "name": "offset",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipes that match.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipeList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Add a recipe",
        "description": "Ingredients and steps are lines, one each, as typed in the editor; a line starting with \"#\" starts a section. Each ingredient line is read into its amount, unit, food and note. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecipeInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "Something isn't right, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/bulk": {
      "post": {
        "operationId": "bulkChangeRecipes",
        "tags": [
          "Recipes"
        ],
        "summary": "Change several recipes at once",
        "description": "Moves recipes to Recently deleted, or adds or takes off tags, up to 500 at a time. Ones that aren't there are skipped. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecipeBulkInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResult"
                }
              }
            }
          },
          "400": {
            "description": "Nothing picked, or something else is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/deleted": {
      "get": {
        "operationId": "listDeletedRecipes",
        "tags": [
          "Recipes"
        ],
        "summary": "Recently deleted",
        "description": "Deleted recipes, which can be put back for 30 days, latest first. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "What's there.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/DeletedRecipe"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "emptyDeletedRecipes",
        "tags": [
          "Recipes"
        ],
        "summary": "Empty Recently deleted",
        "description": "Deletes everything in it for good. This can't be undone. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "responses": {
          "204": {
            "description": "Emptied."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/deleted/bulk": {
      "post": {
        "operationId": "bulkChangeDeletedRecipes",
        "tags": [
          "Recipes"
        ],
        "summary": "Put back, or delete for good, several recipes at once",
        "description": "For recipes in Recently deleted, up to 500 at a time. Ones that aren't there are skipped. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeletedBulkInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkResult"
                }
              }
            }
          },
          "400": {
            "description": "Nothing picked, or something else is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/deleted/{recipeId}": {
      "delete": {
        "operationId": "purgeRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Delete a recipe for good",
        "description": "Only for a recipe in Recently deleted. This can't be undone. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "recipeId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted for good."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe in Recently deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/deleted/{recipeId}/restore": {
      "post": {
        "operationId": "restoreRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Put a recipe back",
        "description": "Takes a recipe out of Recently deleted, back where it was. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "recipeId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Put back.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe in Recently deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import": {
      "post": {
        "operationId": "importRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Add a recipe from a link",
        "description": "Reads the recipe data most recipe sites put in their pages, and its photo. A page without it is read by AI, when the household has AI for importing; otherwise it can't be imported. A link the household imported before gives back that recipe, unless `force` is set. Video links can't be imported yet. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000
                  },
                  "force": {
                    "type": "boolean",
                    "description": "Import it again even if the household has it."
                  }
                },
                "required": [
                  "url"
                ],
                "example": {
                  "url": "https://example.com/lemon-chicken"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already in the library.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "400": {
            "description": "Not a link Merran can fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The page has no recipe data and no AI to read it, AI found no recipe on it, or it's a video.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The site couldn't be reached, or turned Merran away.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/links": {
      "get": {
        "operationId": "listLinkImports",
        "tags": [
          "Recipes"
        ],
        "summary": "The household's link imports still going",
        "description": "Newest first. Finished ones aren't listed. With an API key, it needs the `imports: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:read",
        "responses": {
          "200": {
            "description": "The imports.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkImportList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "importLinks",
        "tags": [
          "Recipes"
        ],
        "summary": "Add recipes from several links",
        "description": "Up to 100 links, imported one at a time in the background: follow along at /recipes/import/links/{importId}, which says how each link went. A link the library has already gives back that recipe rather than a new one. With an API key, it needs the `imports: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkImportRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkImport"
                }
              }
            }
          },
          "400": {
            "description": "None of them is a link Merran can fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/links/check": {
      "post": {
        "operationId": "checkLinks",
        "tags": [
          "Recipes"
        ],
        "summary": "Find the links in some text",
        "description": "The links in pasted text, up to 100, tidied, with which are in the library already and which are videos (which can't be imported yet). Nothing is imported. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkCheckRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The links.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkCheck"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/links/{importId}": {
      "get": {
        "operationId": "getLinkImport",
        "tags": [
          "Recipes"
        ],
        "summary": "How a link import is going",
        "description": "Each link, and where it's got to. With an API key, it needs the `imports: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "importId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The import.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkImport"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such import in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/links/{importId}/stop": {
      "post": {
        "operationId": "stopLinkImport",
        "tags": [
          "Recipes"
        ],
        "summary": "Stop a link import",
        "description": "The recipes it added stay. The link it's on finishes first, so the answer may still say it's running. With an API key, it needs the `imports: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "required": true,
            "name": "importId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkImport"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such import in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/page": {
      "post": {
        "operationId": "importReadPage",
        "tags": [
          "Recipes"
        ],
        "summary": "Add a recipe from a page already open",
        "description": "For the browser extension: a page as the person's own browser shows it, for sites that turn Merran's server away or only show a recipe to someone signed in. Its recipe data and photo, or its text read by AI when the household has AI for importing. A video's page (YouTube, TikTok, Instagram, Facebook) is imported as the video, as `POST /recipes/import` does. A page imported before gives back that recipe. With an API key, it needs the `imports: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReadPage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already in the library.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImportResult"
                }
              }
            }
          },
          "400": {
            "description": "Not a link Merran can keep.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The page is over 2 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The page has no recipe data and no AI to read it, or AI found no recipe on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/text": {
      "post": {
        "operationId": "importRecipeText",
        "tags": [
          "Recipes"
        ],
        "summary": "Add a recipe from text",
        "description": "Text with a recipe in it, from an email, a note or a message, read by AI and added as a recipe. Uses the household's model for importing, and counts towards its AI spend. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TextImport"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "No text, or too much.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No recipe found in it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached, or the AI is busy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The AI provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/import/photos": {
      "post": {
        "operationId": "importRecipePhotos",
        "tags": [
          "Recipes"
        ],
        "summary": "Add a recipe from photos or a PDF",
        "description": "Photos of one recipe, up to 4, in order: a cookbook page or two, a card, a screenshot; or a PDF of it. Read by AI and added as a recipe. JPEG, PNG or WebP up to 15 MB each, or a PDF up to 10 MB. Uses the household's model for importing, which must be able to read photos or PDFs, and counts towards its AI spend. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "photos": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    },
                    "description": "The photos, in order: one `photos` field for each."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "No photos, too many, or not photos Merran can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The photos are too big.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No recipe found in them, or the model can't read photos or PDFs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached, or the AI is busy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The AI provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/nutrition": {
      "get": {
        "operationId": "getNutritionLibrary",
        "tags": [
          "Recipes"
        ],
        "summary": "Nutrition across the library",
        "description": "How many recipes have no nutrition yet, how many need servings first, and the latest run working it out. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "The library.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NutritionLibrary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startNutritionRun",
        "tags": [
          "Recipes"
        ],
        "summary": "Work out nutrition for the library",
        "description": "Every recipe with servings and ingredients but no nutrition, in the background, two at a time: from USDA's data when the server has a key, else estimated by AI. USDA's hourly limit pauses it rather than ending it. One at a time: starting another gives back the one going. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "responses": {
          "202": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NutritionRun"
                }
              }
            }
          },
          "400": {
            "description": "Every recipe with servings has its nutrition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No USDA key on the server, and no AI for the household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/nutrition/{runId}/stop": {
      "post": {
        "operationId": "stopNutritionRun",
        "tags": [
          "Recipes"
        ],
        "summary": "Stop working out nutrition",
        "description": "What it worked out stays. The recipe it's on finishes first, so the answer may still say it's running. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002f"
            },
            "required": true,
            "name": "runId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NutritionRun"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/photos": {
      "get": {
        "operationId": "getPhotoLibrary",
        "tags": [
          "Recipes"
        ],
        "summary": "AI photos across the library",
        "description": "How many recipes have no photo, whether AI photos can be made for the household and why not, its photo style, and the latest run making them. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "The library.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PhotoLibrary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startPhotoRun",
        "tags": [
          "Recipes"
        ],
        "summary": "Make AI photos for the library",
        "description": "A photo for every recipe without one, in the household's style, in the background, on its OpenAI or Google key. One at a time: starting another gives back the one going. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "responses": {
          "202": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PhotoRun"
                }
              }
            }
          },
          "400": {
            "description": "Every recipe has a photo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "AI photos are off on the server, or the household has no key for them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/photos/{runId}/stop": {
      "post": {
        "operationId": "stopPhotoRun",
        "tags": [
          "Recipes"
        ],
        "summary": "Stop making AI photos",
        "description": "The photos made stay. The one being made finishes first, so the answer may still say it's running. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000033"
            },
            "required": true,
            "name": "runId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PhotoRun"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/tags": {
      "get": {
        "operationId": "listTags",
        "tags": [
          "Recipes"
        ],
        "summary": "Recipe tags",
        "description": "The household's tags, with how many recipes have each, alphabetically. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "The tags.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Tag"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/tags/{tagId}": {
      "patch": {
        "operationId": "updateTag",
        "tags": [
          "Recipes"
        ],
        "summary": "Rename or merge a tag",
        "description": "Renaming a tag to another tag's name merges them: every recipe with either has the one tag. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "tagId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TagChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "All the tags, after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Tag"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such tag.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteTag",
        "tags": [
          "Recipes"
        ],
        "summary": "Delete a tag",
        "description": "Takes it off every recipe. The recipes stay. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "tagId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such tag.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/tidy": {
      "get": {
        "operationId": "listTidyRuns",
        "tags": [
          "Review"
        ],
        "summary": "Tidy up runs",
        "description": "The household's runs that tidy many recipes: going, or finished in the last day, newest first, with how far each got and what it found. And how many recipes are still to tidy. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "Its runs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TidyRuns"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startTidyRun",
        "tags": [
          "Review"
        ],
        "summary": "Tidy up many recipes",
        "description": "Tidies recipes in the background, one at a time, with the suggestions left in the Review queue: the ones picked, or every recipe not tidied yet (household admins only), say after moving from another app. It stops if there's no AI, this month's limit is reached, a key runs out of credit, or three recipes in a row fail. Follow it at `GET /recipes/tidy`. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TidyRunInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Started, or the one already going.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TidyRun"
                }
              }
            }
          },
          "400": {
            "description": "Nothing to tidy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/tidy/{runId}/stop": {
      "post": {
        "operationId": "stopTidyRun",
        "tags": [
          "Review"
        ],
        "summary": "Stop a tidy up run",
        "description": "The suggestions it made stay in Review. The recipe it's on finishes first, so the answer may still say it's running. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000002a"
            },
            "required": true,
            "name": "runId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Stopped, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TidyRun"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such run in this household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}": {
      "get": {
        "operationId": "getRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "A recipe",
        "description": "With its ingredients, read into amounts, units and foods, and its steps. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Change a recipe",
        "description": "Replaces the whole recipe, as the editor saves it. The recipe as it was is kept as a version, so the change can be undone; its slug stays the same. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecipeInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "Something isn't right, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Delete a recipe",
        "description": "Moves it to Recently deleted, where it can be put back for 30 days. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Moved to Recently deleted."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/nutrition": {
      "post": {
        "operationId": "workOutNutrition",
        "tags": [
          "Recipes"
        ],
        "summary": "Work out a recipe's nutrition",
        "description": "Per serving, from the ingredients: added up from USDA FoodData Central when the server has a key (AI picking each ingredient's food and weighing what USDA's portions can't), else, or when too much couldn't be counted, estimated by AI. Replaces what it had, from the site too. A recipe's nutrition is worked out by itself after it's saved; this is for doing it again now. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe, with its nutrition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "No servings, or no ingredients.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No USDA key on the server, and no AI for the household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "USDA's limit for the hour, or this month's AI limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/cook-notes": {
      "get": {
        "operationId": "listCookNotes",
        "tags": [
          "Recipes"
        ],
        "summary": "How cooking a recipe went",
        "description": "Each time it was cooked and someone said how it went: stars, a note, or both. Newest first, the latest 50. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The notes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CookNote"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createCookNote",
        "tags": [
          "Recipes"
        ],
        "summary": "Say how cooking a recipe went",
        "description": "Stars, a note for next time, or both. The stars become the recipe's rating, without keeping a version. With AI for cooking notes, a note is turned into a suggested change to the recipe in the background, waiting in the Review queue; `suggesting` says so. An opinion (\"the kids loved it\") is just kept. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CookNoteInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AddedCookNote"
                }
              }
            }
          },
          "400": {
            "description": "Neither stars nor a note, or something else is off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/cook-notes/{noteId}": {
      "delete": {
        "operationId": "deleteCookNote",
        "tags": [
          "Recipes"
        ],
        "summary": "Delete a cooking note",
        "description": "The recipe keeps its rating. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "noteId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe or note.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/diets": {
      "put": {
        "operationId": "setRecipeDiets",
        "tags": [
          "Recipes"
        ],
        "summary": "Correct a recipe's diet labels",
        "description": "The labels are worked out from the ingredients. Corrections that differ from them are kept, and stick when the recipe is edited. It isn't an edit, so no version is kept. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DietsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipe, with its labels.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "A label Merran doesn't know.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/made": {
      "post": {
        "operationId": "markRecipeMade",
        "tags": [
          "Recipes"
        ],
        "summary": "Mark a recipe made",
        "description": "The recipe was made now: today's meal of it on the plan is marked cooked (or, failing that, one in this week's pool becomes today's, cooked), and its `lastMadeAt` is set. Marking that meal not cooked again puts `lastMadeAt` back. It isn't an edit, so no version is kept. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe, made now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/needs": {
      "put": {
        "operationId": "setRecipeNeeds",
        "tags": [
          "Recipes"
        ],
        "summary": "Say what a recipe can't do without",
        "description": "A food the household doesn't like is usually left out of recipes, crossed off and not bought. One a recipe can't do without rules that recipe out instead, as when tidy up marked it needed or, before tidy up, it's in the recipe's name. It isn't an edit, so no version is kept. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NeedsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipe, with what it needs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/pairings": {
      "get": {
        "operationId": "listPairings",
        "tags": [
          "Recipes"
        ],
        "summary": "What a recipe goes with",
        "description": "Recipes it pairs with, picked or by a whole tag, every recipe that adds up to, and the recipes it's offered with. Recipes in Recently deleted are left out. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "What it goes with.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipePairings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "addPairings",
        "tags": [
          "Recipes"
        ],
        "summary": "Add to what a recipe goes with",
        "description": "Recipes, or every recipe with a tag, so new ones join by themselves. Ones it already goes with, and the recipe itself, are skipped. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PairingsInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What it goes with now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipePairings"
                }
              }
            }
          },
          "400": {
            "description": "Nothing picked, or no tag by that name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe, or one of those to pair isn't there.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/pairings/{pairingId}": {
      "delete": {
        "operationId": "removePairing",
        "tags": [
          "Recipes"
        ],
        "summary": "Take something off what a recipe goes with",
        "description": "The recipes stay. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "pairingId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "What it goes with now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipePairings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe or pairing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/photo": {
      "get": {
        "operationId": "getRecipePhoto",
        "tags": [
          "Recipes"
        ],
        "summary": "A recipe's photo",
        "description": "As WebP: `full` for the recipe page, or `card`, cropped for recipe cards. Use the links in the recipe's `photo`, which can be kept for good. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "card"
              ],
              "default": "full"
            },
            "required": false,
            "name": "size",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "description": "Which photo: the links in `photo` say."
            },
            "required": false,
            "description": "Which photo: the links in `photo` say.",
            "name": "v",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The photo.",
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe, or it has no photo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "setRecipePhoto",
        "tags": [
          "Recipes"
        ],
        "summary": "Give a recipe a photo",
        "description": "Send the photo itself as the body, as a JPEG, PNG, WebP or GIF of up to 15 MB. It replaces any photo the recipe had. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "image/*": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipe, with its new photo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "That isn't a photo Merran can read.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The photo is over 15 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRecipePhoto",
        "tags": [
          "Recipes"
        ],
        "summary": "Take a recipe's photo away",
        "description": "The photo is deleted. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe, without a photo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/photo/make": {
      "post": {
        "operationId": "makeRecipePhoto",
        "tags": [
          "Recipes"
        ],
        "summary": "Make a photo with AI",
        "description": "A photo of the finished dish in the household's style, on its OpenAI or Google key: for a recipe without a photo, or in place of one made by AI before. It takes up to a minute. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe, with its new photo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has a real photo, which an AI one never replaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "AI photos are off on the server, or the household has no key for them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/rating": {
      "put": {
        "operationId": "rateRecipe",
        "tags": [
          "Recipes"
        ],
        "summary": "Rate a recipe",
        "description": "Sets the recipe's stars, or clears them. It isn't an edit, so no version is kept. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecipeRatingInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The recipe, with its new rating.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "400": {
            "description": "The rating isn't from 1 to 5.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/stand-ins": {
      "post": {
        "operationId": "suggestStandIns",
        "tags": [
          "Recipes"
        ],
        "summary": "Stand-ins for an ingredient",
        "description": "For an ingredient someone's out of, or doesn't eat: two to four stand-ins that work in this recipe, with amounts for the recipe as written, what's in the pantry first, and nothing the household avoids or that's against its diets. Each says how to use it, and rewrites any step it changes. Nothing is saved; use `POST /recipes/{recipe}/swap` to put one in the recipe. Uses the household's model for swaps, and counts towards its AI spend. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StandInInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stand-ins, best first; maybe none.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Swaps"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe, or no such ingredient in it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached, or the AI is busy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The AI provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/swap": {
      "post": {
        "operationId": "saveSwap",
        "tags": [
          "Recipes"
        ],
        "summary": "Swap an ingredient in a recipe for good",
        "description": "Puts lines in an ingredient's place, and rewrites the steps given, as one change that keeps a version of how it was. The answer has that version's id, to restore to undo it. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveSwapInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Swapped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SavedSwap"
                }
              }
            }
          },
          "400": {
            "description": "No lines, or too many.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The recipe changed since: the ingredient or a step is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/tidy": {
      "get": {
        "operationId": "getRecipeTidy",
        "tags": [
          "Review"
        ],
        "summary": "A recipe's waiting tidy up",
        "description": "The tidy up waiting in the Review queue for this recipe, or null when there's none. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Its suggestion, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipeProposal"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "tidyRecipe",
        "tags": [
          "Review"
        ],
        "summary": "Tidy up a recipe",
        "description": "AI tidies the recipe: consistent ingredient lines, short clear steps, tags from the household's own, a description and servings and times when they're missing, and marks on what it can do without. Nothing changes yet: the suggestion waits in the Review queue, in place of any earlier one for the recipe, and comes back here. Null when there's nothing to change. Uses the household's model for tidying, and counts towards its AI spend. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TidyInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Its suggestion, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipeProposal"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The model can't do it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "This month's AI limit is reached, or the AI is busy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The AI provider failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/versions": {
      "get": {
        "operationId": "listRecipeVersions",
        "tags": [
          "Recipes"
        ],
        "summary": "A recipe's earlier versions",
        "description": "How it was before each change, newest first, so any change can be undone. The last 50 are kept. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The versions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RecipeVersion"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/recipes/{recipe}/versions/{versionId}/restore": {
      "post": {
        "operationId": "restoreRecipeVersion",
        "tags": [
          "Recipes"
        ],
        "summary": "Go back to a version",
        "description": "Makes the recipe what it was in that version. How it is now is kept as a version too, so this can be undone as well. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "description": "The recipe's id, or its slug.",
              "example": "lemon-chicken"
            },
            "required": true,
            "description": "The recipe's id, or its slug.",
            "name": "recipe",
            "in": "path"
          },
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "versionId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The recipe, as it was.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipe"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe or version.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/household": {
      "get": {
        "operationId": "getHouseholdSettings",
        "tags": [
          "Settings"
        ],
        "summary": "The household's settings",
        "description": "The settings of the household in use, like its own AI keys. Anyone in the household can see them; secrets show only whether they're set.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdSettings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateHouseholdSettings",
        "tags": [
          "Settings"
        ],
        "summary": "Change the household's settings",
        "description": "Household admins only. Send only what changes. Nothing is saved if anything is wrong.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SettingChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdSettings"
                }
              }
            }
          },
          "400": {
            "description": "Something isn't right, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server": {
      "get": {
        "operationId": "getServerSettings",
        "tags": [
          "Settings"
        ],
        "summary": "The server's settings",
        "description": "Server admins only. Every server setting that can be seen here, with where its value comes from, and a little about the server.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerSettings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateServerSettings",
        "tags": [
          "Settings"
        ],
        "summary": "Change the server's settings",
        "description": "Server admins only. Send only what changes. Nothing is saved if anything is wrong, and settings from environment variables can't be changed.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SettingChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerSettings"
                }
              }
            }
          },
          "400": {
            "description": "Something isn't right, field by field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai": {
      "get": {
        "operationId": "getServerAi",
        "tags": [
          "AI"
        ],
        "summary": "The server's AI",
        "description": "Server admins only. The server's AI keys and addresses (never the keys themselves, only their last four characters), who may use them and how much, the models they can run, every household's spend on them this month, and the last months'.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The server's AI.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerAi"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai/keys": {
      "post": {
        "operationId": "addServerAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Add an AI key or address for the server",
        "description": "Server admins only. A provider's key replaces the server's one before it; there can be several addresses, which may be on the home network (an Ollama, say). It's tested against the provider first, at no cost, and kept sealed.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewAiKey"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tested and kept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The provider couldn't be reached, or answered with an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai/keys/{keyId}": {
      "delete": {
        "operationId": "removeServerAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Take off one of the server's AI keys",
        "description": "Server admins only. One set in the server's environment can only be taken off there.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Taken off."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It's set in the server's environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai/keys/{keyId}/credit": {
      "put": {
        "operationId": "setServerAiKeyCredit",
        "tags": [
          "AI"
        ],
        "summary": "Note an AI key's credit",
        "description": "Server admins only. The credit on the key now, as its provider's dashboard says: the providers don't tell a key, so Merran counts what it spends from here and warns when about a fifth is left. Entering it also tries the key again if it ran out. Null stops counting.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KeyCredit"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Noted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai/keys/{keyId}/test": {
      "post": {
        "operationId": "testServerAiKey",
        "tags": [
          "AI"
        ],
        "summary": "Test one of the server's AI keys",
        "description": "Server admins only. Asks the provider what the key can use, which costs nothing, and notes it.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "required": true,
            "name": "keyId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "It works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiKey"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The provider couldn't be reached, or answered with an error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/ai/policy": {
      "put": {
        "operationId": "saveAiPolicy",
        "tags": [
          "AI"
        ],
        "summary": "Change who may use the server's AI keys",
        "description": "Server admins only. Which households the server's keys cover, each one's monthly limit on them, the tasks and models they cover, and whether households may add keys and addresses of their own. Households that are gone are dropped.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AiPolicy"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServerAi"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/jobs": {
      "get": {
        "operationId": "listServerJobs",
        "tags": [
          "Settings"
        ],
        "summary": "The server's jobs",
        "description": "Server admins only. The latest 100 jobs, newest first: what's waiting, running, done, or failed and why.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "queued",
                "running",
                "done",
                "failed",
                "cancelled"
              ],
              "description": "Only jobs that are queued, running, done, failed or cancelled."
            },
            "required": false,
            "description": "Only jobs that are queued, running, done, failed or cancelled.",
            "name": "status",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The jobs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/jobs/{jobId}/cancel": {
      "post": {
        "operationId": "cancelServerJob",
        "tags": [
          "Settings"
        ],
        "summary": "Stop a job",
        "description": "Server admins only. A waiting job is cancelled; a running one is asked to stop, and is cancelled once it has.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "jobId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled, or stopping.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It has already finished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/jobs/{jobId}/retry": {
      "post": {
        "operationId": "retryServerJob",
        "tags": [
          "Settings"
        ],
        "summary": "Try a job again",
        "description": "Server admins only. A failed or cancelled job runs again from the start.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "required": true,
            "name": "jobId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "Waiting to run.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It hasn't finished, or another like it is already waiting.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/kroger/test": {
      "post": {
        "operationId": "testKrogerApp",
        "tags": [
          "Settings"
        ],
        "summary": "Test the Kroger app",
        "description": "Server admins only. Asks Kroger for a token with a client ID and secret, before saving them, or with the saved ones.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KrogerAppTest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kroger took them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KrogerAppTested"
                }
              }
            }
          },
          "400": {
            "description": "No client ID and secret to test.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger refused them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Kroger didn't answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/nutrition/test": {
      "post": {
        "operationId": "testUsdaKey",
        "tags": [
          "Settings"
        ],
        "summary": "Test a USDA key",
        "description": "Server admins only. Searches USDA FoodData Central for apples with a key, before saving it, or with the saved one.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UsdaKeyTest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "It works.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsdaKeyTested"
                }
              }
            }
          },
          "400": {
            "description": "No key to test.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "USDA refused the key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "USDA's limit for the hour.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "USDA didn't answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/updates": {
      "get": {
        "operationId": "getUpdates",
        "tags": [
          "Settings"
        ],
        "summary": "Is there a newer Merran?",
        "description": "Server admins only. The version running and the newest release the update check found on GitHub (it looks every 6 hours), with its notes, and whether it's newer.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "What the update check found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Updates"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/updates/check": {
      "post": {
        "operationId": "checkForUpdates",
        "tags": [
          "Settings"
        ],
        "summary": "Look for a newer Merran now",
        "description": "Server admins only. Asks GitHub for Merran's releases now, rather than at the next 6-hourly look, and answers with what it found. Does nothing for a build that isn't a release, or when MERRAN_UPDATE_CHECK says no.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "What the update check found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Updates"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/voice": {
      "get": {
        "operationId": "getVoiceSettings",
        "tags": [
          "Settings"
        ],
        "summary": "The server voice",
        "description": "Server admins only. Who reads aloud, Kokoro's address, the voice picked for each, and the voices Kokoro and OpenAI have. Change them at `PATCH /settings/server` (`voice.*`).",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The server voice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceSettings"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/settings/server/voice/preview": {
      "post": {
        "operationId": "previewVoice",
        "tags": [
          "Settings"
        ],
        "summary": "Hear a voice",
        "description": "Server admins only. A sample cook step read in any Kokoro or OpenAI voice, before it's picked: made once and kept. OpenAI's is on an OpenAI key of the admin's household, a fraction of a cent the first time.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VoicePreview"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The reading, as MP3.",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kokoro didn't answer, or there's no OpenAI key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/share": {
      "post": {
        "operationId": "shareToMerran",
        "tags": [
          "Recipes"
        ],
        "summary": "Share something to Merran",
        "description": "What Android's share sheet and the iPhone Shortcut send. A link to a recipe, as text, a form, JSON, or a file with the link in it (a text file, a .webloc, or the web page itself), is added as a recipe; several links start an import in the background. Without a link, photos, a PDF or text are read by AI: a receipt waits to be looked over for the pantry (`GET /pantry/receipts/{receiptId}`), and a recipe (up to 4 photos, as one) is added, when the household has AI for receipts and importing. It answers with what happened, in words a Shortcut can show, rather than an error when the link couldn't be added. With an API key, it needs the `imports: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "imports:write",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "What was shared, as a file: a text file with the link, a .webloc, the web page, or a photo of a recipe. Several photos come as several `file` fields."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SharedText"
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already there, or not added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareOutcome"
                }
              }
            }
          },
          "201": {
            "description": "Added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareOutcome"
                }
              }
            }
          },
          "202": {
            "description": "Several links, importing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareOutcome"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Too big to share.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites": {
      "get": {
        "operationId": "listSites",
        "tags": [
          "Sites"
        ],
        "summary": "Followed recipe sites",
        "description": "The recipe sites the household follows, oldest followed first, each with its newest recipes; and suggested sites it doesn't follow yet. Merran reads each site's feed every few hours. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "Its sites.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteList"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "followSite",
        "tags": [
          "Sites"
        ],
        "summary": "Follow a recipe site",
        "description": "By any page of the site, its RSS or Atom feed, or a suggestion. Merran finds the feed and reads it straight away when no one's read it lately, opening each new post a few seconds apart and keeping the recipes. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FollowSite"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Following it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FollowedSite"
                }
              }
            }
          },
          "400": {
            "description": "That isn't a link Merran can fetch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The household follows it already.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "No feed was found, or recipe sites are off.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites/recipes": {
      "get": {
        "operationId": "listSiteRecipes",
        "tags": [
          "Sites"
        ],
        "summary": "The newest recipes from followed sites",
        "description": "Up to 24, newest first, from every site the household follows: for Recipes' spotlight and Today's idea for tonight. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "Their newest recipes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteRecipes"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites/recipes/{recipe}/photo": {
      "get": {
        "operationId": "getSiteRecipePhoto",
        "tags": [
          "Sites"
        ],
        "summary": "A site recipe's photo",
        "description": "A small copy, as WebP, fetched once through Merran so browsing doesn't load photos from other sites. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000031"
            },
            "required": true,
            "name": "recipe",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "The photo.",
            "content": {
              "image/webp": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such recipe, or no photo that loads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/sites/{site}": {
      "delete": {
        "operationId": "unfollowSite",
        "tags": [
          "Sites"
        ],
        "summary": "Stop following a site",
        "description": "Recipes imported from it stay in the library. With an API key, it needs the `recipes: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-000000000030"
            },
            "required": true,
            "name": "site",
            "in": "path"
          }
        ],
        "responses": {
          "204": {
            "description": "Not following it any more."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The household doesn't follow that site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores": {
      "get": {
        "operationId": "getStores",
        "tags": [
          "Stores"
        ],
        "summary": "The household's stores",
        "description": "Kroger for the household: whether the server has a Kroger app, the household's store, how orders are collected, and the account linked to send things to its cart. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "Its stores.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoresStatus"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/deals": {
      "get": {
        "operationId": "listDeals",
        "tags": [
          "Stores"
        ],
        "summary": "What's on sale",
        "description": "What's on sale at the household's store, by food, each sale judged against what the product usually costs (the lowest in a while first), and the recipes they're in. From the prices the household has seen this past week: nothing without a Kroger store. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "Its deals.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Deals"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/costs": {
      "get": {
        "operationId": "listRecipeCosts",
        "tags": [
          "Stores"
        ],
        "summary": "What recipes cost",
        "description": "What each recipe costs at the household's Kroger store, from each line's share of its product's price today, where most of its lines have a price; and the household's weekly budget. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "Its recipes' costs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipeCosts"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/list-prices": {
      "get": {
        "operationId": "listItemPrices",
        "tags": [
          "Stores"
        ],
        "summary": "What the list costs",
        "description": "What each thing left to get on the list for the household's Kroger store costs, for the packages needed, and the real deals among them. From the product kept for each food and the last price seen for it, without a look at the store: nothing without a Kroger store. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "Its prices.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListPrices"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/yours": {
      "get": {
        "operationId": "yourStores",
        "tags": [
          "Stores"
        ],
        "summary": "The stores the household shops at",
        "description": "For Find on the list: its Kroger store when it has one and Walmart when the server has it on, which Merran can find products at and send to, then the stores it picked in Settings (`stores.yours`), whose sites can be searched. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "responses": {
          "200": {
            "description": "The household's stores.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/YourStores"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/send-plan": {
      "get": {
        "operationId": "sendPlan",
        "tags": [
          "Stores"
        ],
        "summary": "Where each thing on a list can go",
        "description": "For Send: the stores Merran can send to, and each thing left on the list with the stores the household picked a product at for it, newest first, which is where it goes (asking when there's more than one), and where it's been sent already. Each store's part is then reviewed and sent with its basket's `items`. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "The list being sent: the one the person's looking at.",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001c"
            },
            "required": true,
            "description": "The list being sent: the one the person's looking at.",
            "name": "list",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Where each thing can go.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendPlan"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "There's no such list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger": {
      "put": {
        "operationId": "setKrogerStore",
        "tags": [
          "Stores"
        ],
        "summary": "Pick the household's Kroger store",
        "description": "Household admins only. The store the household shops at (from `GET /stores/kroger/locations`), and whether orders are picked up or delivered. A new store has its prices and sales checked straight away.",
        "security": [
          {
            "session": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/KrogerStoreChanges"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Its stores now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoresStatus"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such Kroger store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/locations": {
      "get": {
        "operationId": "listKrogerLocations",
        "tags": [
          "Stores"
        ],
        "summary": "Kroger stores near a ZIP code",
        "description": "Up to twelve of Kroger's stores, of any banner, near a ZIP code. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{5}$",
              "example": "85016"
            },
            "required": true,
            "name": "zip",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Stores near it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreLocations"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/link": {
      "post": {
        "operationId": "startKrogerLink",
        "tags": [
          "Stores"
        ],
        "summary": "Link a Kroger account",
        "description": "Household admins only. Where to send the person to let Merran add to their Kroger cart; Kroger sends them back to `GET /stores/kroger/callback`. One account for the household, so everyone shops one cart.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Where to go.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KrogerLink"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "unlinkKroger",
        "tags": [
          "Stores"
        ],
        "summary": "Unlink the Kroger account",
        "description": "Household admins only. Its tokens are deleted.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "204": {
            "description": "Unlinked."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/callback": {
      "get": {
        "operationId": "krogerCallback",
        "tags": [
          "Stores"
        ],
        "summary": "Where Kroger sends the person back",
        "description": "Not called directly: the redirect address the Kroger developer app has. It finishes linking the account, then sends the person on to Settings, Household, saying how it went.",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "maxLength": 2000
            },
            "required": false,
            "name": "code",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "required": false,
            "name": "state",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "required": false,
            "name": "error",
            "in": "query"
          }
        ],
        "responses": {
          "302": {
            "description": "On to Settings, Household."
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/basket": {
      "get": {
        "operationId": "getKrogerBasket",
        "tags": [
          "Stores"
        ],
        "summary": "The Kroger cart to look over",
        "description": "Each thing left to get (the ones asked for, which Send has going to Kroger; or without them, everything left on every list) with its product: the one the household picked before, at today's price, or Merran's pick from the store's search, made by the household's model for shopping when it has AI. How many packages, worked out from the product's size where it can be. Nothing is sent until `POST /stores/kroger/send`. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f-]{36}(,[0-9a-f-]{36}){0,299}$/i",
              "description": "Just these things on the list, by id, separated by commas: the ones Send has going to this store. Without it, everything left on every list.",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d,0199e8b0-5a10-7a01-8b00-00000000001e"
            },
            "required": false,
            "description": "Just these things on the list, by id, separated by commas: the ones Send has going to this store. Without it, everything left on every list.",
            "name": "items",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The cart to look over.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Basket"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up, or the household has no store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/search": {
      "get": {
        "operationId": "searchKroger",
        "tags": [
          "Stores"
        ],
        "summary": "Search the household's Kroger store",
        "description": "Up to twenty products, with today's prices, to pick another. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 2,
              "maxLength": 100,
              "example": "milk"
            },
            "required": true,
            "name": "q",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "What the store has.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreProducts"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up, or the household has no store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/products/{itemId}": {
      "put": {
        "operationId": "pickKrogerProduct",
        "tags": [
          "Stores"
        ],
        "summary": "Pick a product for something on the list",
        "description": "Kept for that food from now on, at the household's store, and never replaced by itself. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PickProduct"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Its line, with the product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BasketLine"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such thing on the list, or product at the store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/send": {
      "post": {
        "operationId": "sendKrogerBasket",
        "tags": [
          "Stores"
        ],
        "summary": "Send to the Kroger cart",
        "description": "Adds the lines given to the linked account's cart, so many of each, by barcode. Their products are kept as the household's from now on. Kroger's API can only add to a cart: checking out is on Kroger's site. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendBasket"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SentBasket"
                }
              }
            }
          },
          "400": {
            "description": "None of them has a product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Kroger stopped taking the account: link it again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/kroger/prices": {
      "post": {
        "operationId": "checkKrogerPrices",
        "tags": [
          "Stores"
        ],
        "summary": "Check prices now",
        "description": "Household admins only. What's done each morning by itself: today's prices for the products the household keeps, and a look at the store for foods its recipes use, for their sales.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "Checked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceCheck"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Kroger isn't set up, or there's no store picked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Kroger's daily limit for the app is used up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Kroger didn't answer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/walmart/basket": {
      "get": {
        "operationId": "getWalmartBasket",
        "tags": [
          "Stores"
        ],
        "summary": "The Walmart cart to look over",
        "description": "Experimental, when the server admin has turned Walmart on. Each thing left to get (the ones asked for, which Send has going to Walmart; or without them, everything left on every list) with its product, the household's own pick or Merran's, how many and what it cost when last seen; and what's still to find at Walmart, which Merran's browser extension looks for in the person's own Walmart tab. Merran never reaches Walmart itself. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f-]{36}(,[0-9a-f-]{36}){0,299}$/i",
              "description": "Just these things on the list, by id, separated by commas: the ones Send has going to this store. Without it, everything left on every list.",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d,0199e8b0-5a10-7a01-8b00-00000000001e"
            },
            "required": false,
            "description": "Just these things on the list, by id, separated by commas: the ones Send has going to this store. Without it, everything left on every list.",
            "name": "items",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "The cart to look over.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalmartBasket"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Walmart isn't turned on on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/walmart/found": {
      "post": {
        "operationId": "walmartFound",
        "tags": [
          "Stores"
        ],
        "summary": "What the extension found at Walmart",
        "description": "The products Merran's browser extension read from Walmart's search for things on the list. Every price is kept; each thing without a product of the household's own gets Merran's pick (the household's model for shopping when it has AI, else the first in stock), kept for its food. A product someone picked is never replaced, only brought up to date. The answer is the cart to look over. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalmartFound"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Picked, and the cart as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalmartBasket"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Walmart isn't turned on on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/walmart/products/{itemId}": {
      "get": {
        "operationId": "walmartOptions",
        "tags": [
          "Stores"
        ],
        "summary": "What Walmart had for a thing on the list",
        "description": "The products Walmart's search had for its food when Merran's browser extension last looked, to pick another from without looking again. Empty if it hasn't looked yet. With an API key, it needs the `list: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:read",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "responses": {
          "200": {
            "description": "What Walmart had.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalmartOptions"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Walmart isn't turned on on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "pickWalmartProduct",
        "tags": [
          "Stores"
        ],
        "summary": "Buy this at Walmart for a thing on the list",
        "description": "A product someone picked from Walmart, as the extension read it: kept for the thing's food from now on, and never replaced by Merran's picks. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "0199e8b0-5a10-7a01-8b00-00000000001d"
            },
            "required": true,
            "name": "itemId",
            "in": "path"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalmartPick"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Kept, and the line as it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BasketLine"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not on the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Walmart isn't turned on on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/stores/walmart/sent": {
      "post": {
        "operationId": "walmartSent",
        "tags": [
          "Stores"
        ],
        "summary": "What the extension put in the Walmart cart",
        "description": "Remembered as sent while it's still on the list, so it's left unticked next time, and its product kept as the household's, as its page showed it. The extension adds to the cart in the person's own Walmart tab; this only records it. With an API key, it needs the `list: write` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "list:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalmartSent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Remembered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SentBasket"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Walmart isn't turned on on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/telemetry/install-id": {
      "post": {
        "operationId": "renewInstallId",
        "tags": [
          "Settings"
        ],
        "summary": "Make a new install id",
        "description": "Server admins only. Summaries from now on can't be linked to earlier ones. Answers with the next summary.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The next summary, with the new id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryReport"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/telemetry/report": {
      "get": {
        "operationId": "getTelemetryReport",
        "tags": [
          "Settings"
        ],
        "summary": "The next telemetry summary",
        "description": "Server admins only. Exactly what the daily summary would send, whether or not it's on.",
        "security": [
          {
            "session": []
          }
        ],
        "responses": {
          "200": {
            "description": "The summary.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TelemetryReport"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/voice": {
      "get": {
        "operationId": "getVoice",
        "tags": [
          "Voice"
        ],
        "summary": "Whether the server reads aloud",
        "description": "For the cook view: whether the server reads steps aloud for the household (Kokoro, or OpenAI on one of its keys), or each device reads in its own voice. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "responses": {
          "200": {
            "description": "The household's voice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HouseholdVoice"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/voice/speak": {
      "post": {
        "operationId": "speak",
        "tags": [
          "Voice"
        ],
        "summary": "Read some text aloud",
        "description": "In the server's voice, as MP3: a cook step, an amount, a timer. Each piece of text is read once per voice and kept, so it's never paid for twice. When the server doesn't read aloud for the household, it says so, and the device reads in its own voice. With an API key, it needs the `recipes: read` permission.",
        "security": [
          {
            "session": []
          },
          {
            "apiKey": []
          }
        ],
        "x-merran-permission": "recipes:read",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Speak"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The reading, as MP3.",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not allowed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The server doesn't read aloud for the household.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/sign-in/username": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "With a username and password. Sets the session cookie that signed-in requests send. Scripts and tools like Home Assistant use an API key instead. Answers in Better Auth's error shape.",
        "operationId": "signIn",
        "security": [],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "username": {
                    "type": "string",
                    "description": "The username of the user"
                  },
                  "password": {
                    "type": "string",
                    "description": "The password of the user"
                  },
                  "rememberMe": {
                    "type": "boolean",
                    "description": "Remember the user session"
                  },
                  "callbackURL": {
                    "type": "string",
                    "description": "URL to redirect to after sign in (also used as the redirect target for email verification when required)"
                  }
                },
                "required": [
                  "username",
                  "password"
                ]
              },
              "example": {
                "username": "sam",
                "password": "correct horse battery staple",
                "rememberMe": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redirect": {
                      "type": "boolean",
                      "description": "Whether the client should follow the Location header. True when callbackURL was provided."
                    },
                    "token": {
                      "type": "string",
                      "description": "Session token for the authenticated session"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true,
                      "description": "The callbackURL echoed back so the client can redirect."
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "redirect",
                    "token",
                    "user"
                  ]
                },
                "example": {
                  "redirect": false,
                  "token": "sample-session-token",
                  "url": null,
                  "user": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "name": "Sam Rivera",
                    "email": "sam@example.com",
                    "emailVerified": false,
                    "image": null,
                    "createdAt": "2026-10-01T17:30:00.000Z",
                    "updatedAt": "2026-10-01T17:30:00.000Z",
                    "username": "sam",
                    "displayUsername": "sam",
                    "role": "admin",
                    "banned": false
                  }
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "422": {
            "description": "Unprocessable Entity. Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign in"
      }
    },
    "/api/auth/sign-in/email": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "The same as signing in with a username, with the account's email address instead.",
        "operationId": "signInWithEmail",
        "security": [],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email of the user"
                  },
                  "password": {
                    "type": "string",
                    "description": "Password of the user"
                  },
                  "callbackURL": {
                    "type": "string",
                    "description": "Callback URL to use as a redirect for email verification"
                  },
                  "rememberMe": {
                    "type": "boolean",
                    "description": "If this is false, the session will not be remembered. Default is `true`."
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              },
              "example": {
                "email": "sam@example.com",
                "password": "correct horse battery staple"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success - Returns either session details or redirect URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Session response when idToken is provided",
                  "properties": {
                    "redirect": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "token": {
                      "type": "string",
                      "description": "Session token"
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "user": {
                      "type": "object",
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "redirect",
                    "token",
                    "user"
                  ]
                },
                "example": {
                  "redirect": false,
                  "token": "sample-session-token",
                  "url": null,
                  "user": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "name": "Sam Rivera",
                    "email": "sam@example.com",
                    "emailVerified": false,
                    "image": null,
                    "createdAt": "2026-10-01T17:30:00.000Z",
                    "updatedAt": "2026-10-01T17:30:00.000Z",
                    "username": "sam",
                    "displayUsername": "sam",
                    "role": "admin",
                    "banned": false
                  }
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign in with an email address"
      }
    },
    "/api/auth/sign-out": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Ends this session, and clears its cookie.",
        "operationId": "signOut",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "callbackURL": {
                    "type": "string",
                    "description": "The URL to redirect to after provider logout"
                  },
                  "disableRedirect": {
                    "type": "boolean",
                    "description": "Return the provider logout URL without redirecting"
                  },
                  "state": {
                    "type": "string",
                    "description": "State to pass to the provider logout endpoint"
                  }
                }
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "url": {
                      "type": "string",
                      "description": "Provider logout URL when RP-initiated logout is available"
                    },
                    "redirect": {
                      "type": "boolean",
                      "description": "Whether the client should redirect to the provider logout URL"
                    }
                  }
                },
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign out"
      }
    },
    "/api/auth/get-session": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "description": "Who's signed in, and their session, or null when no one is. `GET /api/auth/me` says more: their households and which one is in use.",
        "operationId": "getSession",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/Session"
                    },
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  },
                  "required": [
                    "session",
                    "user"
                  ]
                },
                "example": {
                  "session": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000010",
                    "expiresAt": "2026-10-22T18:30:00.000Z",
                    "token": "sample-session-token",
                    "createdAt": "2026-10-15T18:30:00.000Z",
                    "updatedAt": "2026-10-15T18:30:00.000Z",
                    "ipAddress": "192.168.1.20",
                    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X)",
                    "userId": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "activeOrganizationId": "0199e8b0-5a10-7a01-8b00-000000000003"
                  },
                  "user": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "name": "Sam Rivera",
                    "email": "sam@example.com",
                    "emailVerified": false,
                    "image": null,
                    "createdAt": "2026-10-01T17:30:00.000Z",
                    "updatedAt": "2026-10-01T17:30:00.000Z",
                    "username": "sam",
                    "displayUsername": "sam",
                    "role": "admin",
                    "banned": false
                  }
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "This session"
      }
    },
    "/api/auth/list-sessions": {
      "get": {
        "tags": [
          "Sign-in"
        ],
        "description": "Every session you have open: one for each browser or device.",
        "operationId": "listSessions",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Session"
                  }
                },
                "example": [
                  {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000010",
                    "expiresAt": "2026-10-22T18:30:00.000Z",
                    "token": "sample-session-token",
                    "createdAt": "2026-10-15T18:30:00.000Z",
                    "updatedAt": "2026-10-15T18:30:00.000Z",
                    "ipAddress": "192.168.1.20",
                    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X)",
                    "userId": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "activeOrganizationId": "0199e8b0-5a10-7a01-8b00-000000000003"
                  }
                ]
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Where you're signed in"
      }
    },
    "/api/auth/revoke-session": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Ends one of your sessions, named by its token from `GET /api/auth/list-sessions`.",
        "operationId": "revokeSession",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The token to revoke"
                  }
                },
                "required": [
                  "token"
                ]
              },
              "example": {
                "token": "sample-session-token"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "description": "Indicates if the session was revoked successfully"
                    }
                  },
                  "required": [
                    "status"
                  ]
                },
                "example": {
                  "status": true
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign out somewhere"
      }
    },
    "/api/auth/revoke-other-sessions": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Ends every session you have but this one.",
        "operationId": "revokeOtherSessions",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "description": "Indicates if all other sessions were revoked successfully"
                    }
                  },
                  "required": [
                    "status"
                  ]
                },
                "example": {
                  "status": true
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign out everywhere else"
      }
    },
    "/api/auth/revoke-sessions": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Ends every session you have, this one too.",
        "operationId": "revokeAllSessions",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "boolean",
                      "description": "Indicates if all sessions were revoked successfully"
                    }
                  },
                  "required": [
                    "status"
                  ]
                },
                "example": {
                  "status": true
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Sign out everywhere"
      }
    },
    "/api/auth/change-password": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Needs your current password. The new one needs at least 10 characters. With `revokeOtherSessions`, everywhere else is signed out too, and a new token comes back for this session.",
        "operationId": "changePassword",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "newPassword": {
                    "type": "string",
                    "description": "The new password to set"
                  },
                  "currentPassword": {
                    "type": "string",
                    "description": "The current password is required"
                  },
                  "revokeOtherSessions": {
                    "type": "boolean",
                    "description": "Must be a boolean value"
                  }
                },
                "required": [
                  "newPassword",
                  "currentPassword"
                ]
              },
              "example": {
                "currentPassword": "correct horse battery staple",
                "newPassword": "a longer, newer passphrase",
                "revokeOtherSessions": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password successfully changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "token": {
                      "type": "string",
                      "nullable": true,
                      "description": "New session token if other sessions were revoked"
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "The unique identifier of the user"
                        },
                        "email": {
                          "type": "string",
                          "format": "email",
                          "description": "The email address of the user"
                        },
                        "name": {
                          "type": "string",
                          "description": "The name of the user"
                        },
                        "image": {
                          "type": "string",
                          "format": "uri",
                          "nullable": true,
                          "description": "The profile image URL of the user"
                        },
                        "emailVerified": {
                          "type": "boolean",
                          "description": "Whether the email has been verified"
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the user was created"
                        },
                        "updatedAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the user was last updated"
                        }
                      },
                      "required": [
                        "id",
                        "email",
                        "name",
                        "emailVerified",
                        "createdAt",
                        "updatedAt"
                      ]
                    }
                  },
                  "required": [
                    "user"
                  ]
                },
                "example": {
                  "token": "sample-session-token",
                  "user": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "name": "Sam Rivera",
                    "email": "sam@example.com",
                    "emailVerified": false,
                    "image": null,
                    "createdAt": "2026-10-01T17:30:00.000Z",
                    "updatedAt": "2026-10-01T17:30:00.000Z",
                    "username": "sam",
                    "displayUsername": "sam",
                    "role": "admin",
                    "banned": false
                  }
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Change your password"
      }
    },
    "/api/auth/update-user": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Your name, username or picture.",
        "operationId": "updateProfile",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The name of the user"
                  },
                  "image": {
                    "type": "string",
                    "description": "The image of the user",
                    "nullable": true
                  },
                  "username": {
                    "type": "string"
                  },
                  "displayUsername": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "Sam R."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "$ref": "#/components/schemas/User"
                    }
                  }
                },
                "example": {
                  "user": {
                    "id": "0199e8b0-5a10-7a01-8b00-000000000001",
                    "name": "Sam R.",
                    "email": "sam@example.com",
                    "emailVerified": false,
                    "image": null,
                    "createdAt": "2026-10-01T17:30:00.000Z",
                    "updatedAt": "2026-10-01T17:30:00.000Z",
                    "username": "sam",
                    "displayUsername": "sam",
                    "role": "admin",
                    "banned": false
                  }
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Change your name"
      }
    },
    "/api/auth/organization/set-active": {
      "post": {
        "tags": [
          "Sign-in"
        ],
        "description": "Makes another of your households the one in use on this device. Better Auth calls households organizations, so it's named by `organizationId`. An API key always works in its own household.",
        "operationId": "switchHousehold",
        "security": [
          {
            "session": []
          }
        ],
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "organizationId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The organization id to set as active. It can be null to unset the active organization. Eg: \"org-id\""
                  },
                  "organizationSlug": {
                    "type": "string",
                    "description": "The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: \"org-slug\""
                  }
                }
              },
              "example": {
                "organizationId": "0199e8b0-5a10-7a01-8b00-000000000003"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The organization",
                  "$ref": "#/components/schemas/Organization"
                },
                "example": {
                  "id": "0199e8b0-5a10-7a01-8b00-000000000003",
                  "name": "The Riveras",
                  "slug": "the-riveras",
                  "logo": null,
                  "createdAt": "2026-10-01T17:30:00.000Z",
                  "metadata": null
                }
              }
            }
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Bad Request. Usually due to missing parameters, or invalid parameters."
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "message"
                  ]
                }
              }
            },
            "description": "Unauthorized. Due to missing or invalid authentication."
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Forbidden. You do not have permission to access this resource or to perform this action."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Not Found. The requested resource was not found."
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Too Many Requests. You have exceeded the rate limit. Try again later."
          },
          "500": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            },
            "description": "Internal Server Error. This is a problem with the server that you cannot fix."
          }
        },
        "summary": "Switch households"
      }
    }
  },
  "webhooks": {}
}
