{
  "openapi": "3.1.0",
  "info": {
    "title": "BookNDish API",
    "version": "1.0.0",
    "summary": "Work with one restaurant: its menu, orders, bookings, guests and sales.",
    "description": "Every request acts for the restaurant that owns the bearer token and goes through the same rules as the admin: prices come from the menu, the owner's ordering and booking rules apply, status changes follow the order's lifecycle, and writes are audited. Money is an integer in the currency's minor unit (`price_minor: 1250` is 12.50 EUR, 12.500 TND). Names and descriptions are translation maps (`{\"en\": \"Soup\", \"fr\": \"Soupe\"}`). Times are ISO 8601 with offset. v1 only grows: new endpoints, new optional fields, new webhook events.",
    "contact": { "name": "BookNDish developers", "url": "/developers" }
  },
  "servers": [
    { "url": "/api/v1", "description": "This BookNDish host. A sandbox token (bnd_sbx_) works on the same address and acts for its sample venue." }
  ],
  "security": [{ "bearerAuth": [] }, { "oauth2": [] }],
  "tags": [
    { "name": "Menu", "description": "Menus, categories and dishes." },
    { "name": "Orders", "description": "Orders on any channel, and their status." },
    { "name": "Reservations", "description": "Bookings and table availability. Needs a plan with reservations." },
    { "name": "Guests", "description": "Guest profiles, with contact details only for guests who agreed to share them." },
    { "name": "Reports", "description": "Sales figures. Needs a plan with reporting." },
    { "name": "OAuth", "description": "Apps from the marketplace: a venue owner allows an app on the consent screen, and the app exchanges the code for an access token (used as the bearer token of every operation above) and a refresh token. Authorization code with PKCE (S256)." }
  ],
  "paths": {
    "/menus": {
      "get": {
        "tags": ["Menu"],
        "operationId": "listMenus",
        "summary": "Every menu with its categories",
        "x-scope": "menu:read",
        "responses": {
          "200": {
            "description": "The menus, in the owner's order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/MenuWithCategories" } },
                    "meta": { "type": "object", "required": ["count", "currency"], "properties": { "count": { "type": "integer" }, "currency": { "$ref": "#/components/schemas/Currency" } } },
                    "links": { "$ref": "#/components/schemas/NoLinks" }
                  }
                },
                "example": {
                  "data": [{ "id": 3, "name": { "en": "Dinner" }, "is_active": true, "sort_order": 0, "categories": [{ "id": 7, "menu_id": 3, "name": { "en": "Mains" }, "is_active": true, "sort_order": 0 }] }],
                  "meta": { "count": 1, "currency": "EUR" },
                  "links": {}
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/menus/{menu}": {
      "get": {
        "tags": ["Menu"],
        "operationId": "getMenu",
        "summary": "One menu, its categories and their dishes",
        "description": "`meta.now` says what menus by time of day and happy hours make of the menu right now, on the default branch's clock: whether it and each category are open, and each dish or size a happy hour has cut, at the price charged now.",
        "x-scope": "menu:read",
        "parameters": [{ "$ref": "#/components/parameters/MenuId" }],
        "responses": {
          "200": {
            "description": "The menu.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/MenuWithItems" },
                    "meta": {
                      "type": "object",
                      "required": ["currency", "now"],
                      "properties": {
                        "currency": { "$ref": "#/components/schemas/Currency" },
                        "now": {
                          "type": "object",
                          "required": ["available", "closed_category_ids", "prices"],
                          "properties": {
                            "available": { "type": "boolean" },
                            "closed_category_ids": { "type": "array", "items": { "type": "integer" } },
                            "prices": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "required": ["item_id", "variant_id", "price_minor"],
                                "properties": { "item_id": { "type": "integer" }, "variant_id": { "type": ["integer", "null"] }, "price_minor": { "type": "integer" } }
                              }
                            }
                          }
                        }
                      }
                    },
                    "links": { "$ref": "#/components/schemas/NoLinks" }
                  }
                },
                "example": {
                  "data": {
                    "id": 3, "name": { "en": "Dinner" }, "is_active": true, "sort_order": 0,
                    "categories": [{
                      "id": 7, "menu_id": 3, "name": { "en": "Mains" }, "is_active": true, "sort_order": 0,
                      "items": [{
                        "id": 42, "category_id": 7, "name": { "en": "Burger" }, "description": { "en": "Beef, brioche" },
                        "price_minor": 1100, "is_available": true, "show_online": true, "dietary": "none", "allergens": ["gluten", "milk"], "sort_order": 1,
                        "variants": [], "modifier_groups": [], "updated_at": "2026-09-29T10:12:00+00:00"
                      }]
                    }]
                  },
                  "meta": { "currency": "EUR", "now": { "available": true, "closed_category_ids": [], "prices": [] } },
                  "links": {}
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/categories": {
      "get": {
        "tags": ["Menu"],
        "operationId": "listCategories",
        "summary": "Every category, or one menu's",
        "x-scope": "menu:read",
        "parameters": [{ "name": "menu_id", "in": "query", "required": false, "schema": { "type": "integer" }, "description": "Only this menu's categories." }],
        "responses": {
          "200": {
            "description": "The categories.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } },
                    "meta": { "type": "object", "required": ["count"], "properties": { "count": { "type": "integer" } } },
                    "links": { "$ref": "#/components/schemas/NoLinks" }
                  }
                },
                "example": { "data": [{ "id": 7, "menu_id": 3, "name": { "en": "Mains" }, "is_active": true, "sort_order": 0 }], "meta": { "count": 1 }, "links": {} }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/items": {
      "get": {
        "tags": ["Menu"],
        "operationId": "listItems",
        "summary": "Dishes, a page at a time",
        "x-scope": "menu:read",
        "parameters": [
          { "name": "category_id", "in": "query", "required": false, "schema": { "type": "integer" } },
          { "name": "available", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true", "false", "1", "0"] } },
          { "$ref": "#/components/parameters/PerPage" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of dishes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } },
                    "meta": { "$ref": "#/components/schemas/PageMetaWithCurrency" },
                    "links": { "$ref": "#/components/schemas/PageLinks" }
                  }
                },
                "example": {
                  "data": [{
                    "id": 42, "category_id": 7, "name": { "en": "Burger" }, "description": null, "price_minor": 1100, "is_available": true,
                    "show_online": true, "dietary": "none", "allergens": null, "sort_order": 1, "variants": [], "modifier_groups": [], "updated_at": "2026-09-29T10:12:00+00:00"
                  }],
                  "meta": { "currency": "EUR", "per_page": 25, "next_cursor": null, "prev_cursor": null },
                  "links": { "next": null, "prev": null }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Menu"],
        "operationId": "createItem",
        "summary": "Add a dish",
        "description": "`name` and `description` are a string in the restaurant's default language, or a translation map. A dish is priced by `price_minor`, or per size by `variants`.",
        "x-scope": "menu:write",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["category_id", "name"],
                "properties": {
                  "category_id": { "type": "integer" },
                  "name": { "$ref": "#/components/schemas/TextInput" },
                  "description": { "oneOf": [{ "$ref": "#/components/schemas/TextInput" }, { "type": "null" }] },
                  "price_minor": { "type": ["integer", "null"], "minimum": 0 },
                  "variants": {
                    "type": "array",
                    "maxItems": 20,
                    "items": { "type": "object", "required": ["name", "price_minor"], "properties": { "name": { "$ref": "#/components/schemas/Translations" }, "price_minor": { "type": "integer", "minimum": 0 }, "is_default": { "type": "boolean" } } }
                  },
                  "dietary": { "$ref": "#/components/schemas/Dietary" },
                  "allergens": { "type": ["array", "null"], "items": { "$ref": "#/components/schemas/Allergen" } },
                  "is_available": { "type": "boolean" },
                  "show_online": { "type": "boolean" },
                  "modifier_group_ids": { "type": "array", "items": { "type": "integer" } }
                }
              },
              "example": { "category_id": 7, "name": "Burger", "price_minor": 1100, "allergens": ["gluten"] }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/ItemResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/items/{item}": {
      "get": {
        "tags": ["Menu"],
        "operationId": "getItem",
        "summary": "One dish",
        "x-scope": "menu:read",
        "parameters": [{ "$ref": "#/components/parameters/ItemId" }],
        "responses": {
          "200": { "$ref": "#/components/responses/ItemResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "tags": ["Menu"],
        "operationId": "updateItem",
        "summary": "Change a dish",
        "description": "Only the fields sent change; variants and modifier groups are kept. A price change is recorded in the activity log. A dish priced per size refuses `price_minor`.",
        "x-scope": "menu:write",
        "parameters": [{ "$ref": "#/components/parameters/ItemId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "category_id": { "type": "integer" },
                  "name": { "$ref": "#/components/schemas/TextInput" },
                  "description": { "oneOf": [{ "$ref": "#/components/schemas/TextInput" }, { "type": "null" }] },
                  "price_minor": { "type": "integer", "minimum": 0 },
                  "dietary": { "$ref": "#/components/schemas/Dietary" },
                  "allergens": { "type": ["array", "null"], "items": { "$ref": "#/components/schemas/Allergen" } },
                  "is_available": { "type": "boolean" },
                  "show_online": { "type": "boolean" }
                }
              },
              "example": { "price_minor": 1250 }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ItemResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/items/{item}/availability": {
      "put": {
        "tags": ["Menu"],
        "operationId": "setItemAvailability",
        "summary": "Sold out, and back",
        "description": "Recorded like a flip on the till.",
        "x-scope": "menu:write",
        "parameters": [{ "$ref": "#/components/parameters/ItemId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["is_available"], "properties": { "is_available": { "type": "boolean" } } },
              "example": { "is_available": false }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/ItemResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders": {
      "get": {
        "tags": ["Orders"],
        "operationId": "listOrders",
        "summary": "Orders, newest first",
        "x-scope": "orders:read",
        "parameters": [
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string" }, "description": "One status or a comma list: `placed,accepted`." },
          { "name": "type", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/OrderType" } },
          { "name": "channel", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "placed_from", "in": "query", "required": false, "schema": { "type": "string" }, "description": "A date or date-time." },
          { "name": "placed_to", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "updated_since", "in": "query", "required": false, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/PerPage" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of orders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Order" } },
                    "meta": { "$ref": "#/components/schemas/PageMeta" },
                    "links": { "$ref": "#/components/schemas/PageLinks" }
                  }
                },
                "example": { "data": [], "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null }, "links": { "next": null, "prev": null } }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Orders"],
        "operationId": "createOrder",
        "summary": "Place an order",
        "description": "The order is priced from the menu (any price in the body is ignored), the owner's ordering rules apply (types offered, minimum order, delivery zones, opening hours; an order outside hours is held for the next opening), the kitchen gets its tickets and a connected POS gets the order. Dine-in orders need `table_code`.",
        "x-scope": "orders:write",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["type", "customer_name", "customer_phone", "lines"],
                "properties": {
                  "type": { "$ref": "#/components/schemas/OrderType" },
                  "channel": { "type": "string", "enum": ["web", "phone", "whatsapp"] },
                  "table_code": { "type": ["string", "null"] },
                  "customer_name": { "type": "string", "maxLength": 120 },
                  "customer_phone": { "type": "string", "maxLength": 40 },
                  "customer_email": { "type": ["string", "null"], "format": "email" },
                  "note": { "type": ["string", "null"], "maxLength": 500 },
                  "scheduled_for": { "type": ["string", "null"], "format": "date-time" },
                  "delivery_address": { "type": ["object", "null"], "properties": { "line1": { "type": "string" }, "city": { "type": "string" }, "phone": { "type": "string" } } },
                  "lines": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "required": ["item_id", "quantity"],
                      "properties": {
                        "item_id": { "type": "integer" },
                        "variant_id": { "type": ["integer", "null"] },
                        "quantity": { "type": "integer", "minimum": 1, "maximum": 99 },
                        "option_ids": { "type": "array", "items": { "type": "integer" } }
                      }
                    }
                  }
                }
              },
              "example": {
                "type": "delivery", "channel": "web", "customer_name": "Ada Lovelace", "customer_phone": "+15550100", "customer_email": "ada@example.com",
                "note": "Ring twice", "delivery_address": { "line1": "12 Main St", "city": "Springfield" },
                "lines": [{ "item_id": 42, "quantity": 2, "option_ids": [9] }, { "item_id": 43, "variant_id": 11, "quantity": 1 }]
              }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/OrderResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders/{order}": {
      "get": {
        "tags": ["Orders"],
        "operationId": "getOrder",
        "summary": "One order",
        "x-scope": "orders:read",
        "parameters": [{ "$ref": "#/components/parameters/OrderId" }],
        "responses": {
          "200": { "$ref": "#/components/responses/OrderResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/orders/{order}/status": {
      "post": {
        "tags": ["Orders"],
        "operationId": "changeOrderStatus",
        "summary": "Move an order one step",
        "description": "`placed` to `accepted` or `rejected`, `accepted` to `ready`, `ready` to `completed`, and anything not finished to `cancelled`. Accepting fires the kitchen. A cancel with a `reason` (3 to 200 characters) also voids the kitchen tickets and keeps the reason.",
        "x-scope": "orders:write",
        "parameters": [{ "$ref": "#/components/parameters/OrderId" }, { "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["status"],
                "properties": {
                  "status": { "type": "string", "enum": ["accepted", "rejected", "ready", "completed", "cancelled"] },
                  "reason": { "type": ["string", "null"], "minLength": 3, "maxLength": 200 }
                }
              },
              "example": { "status": "accepted" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/OrderResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/reservations": {
      "get": {
        "tags": ["Reservations"],
        "operationId": "listReservations",
        "summary": "Bookings for a day or a range",
        "x-scope": "reservations:read",
        "parameters": [
          { "name": "date", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "status", "in": "query", "required": false, "schema": { "$ref": "#/components/schemas/ReservationStatus" } },
          { "$ref": "#/components/parameters/PerPage" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of bookings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Reservation" } },
                    "meta": { "$ref": "#/components/schemas/PageMeta" },
                    "links": { "$ref": "#/components/schemas/PageLinks" }
                  }
                },
                "example": { "data": [], "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null }, "links": { "next": null, "prev": null } }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "tags": ["Reservations"],
        "operationId": "createReservation",
        "summary": "Write a booking down",
        "description": "The booking rules apply (party size, lead time, closing days); the guest gets the same confirmation as for a phone booking.",
        "x-scope": "reservations:write",
        "parameters": [{ "$ref": "#/components/parameters/IdempotencyKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "phone", "party_size", "date", "time"],
                "properties": {
                  "name": { "type": "string", "maxLength": 120 },
                  "phone": { "type": "string", "maxLength": 40 },
                  "email": { "type": ["string", "null"], "format": "email" },
                  "party_size": { "type": "integer", "minimum": 1 },
                  "date": { "type": "string", "format": "date" },
                  "time": { "type": "string", "pattern": "^[0-2][0-9]:[0-5][0-9]$" },
                  "table_id": { "type": ["integer", "null"] },
                  "note": { "type": ["string", "null"], "maxLength": 500 },
                  "status": { "type": "string", "enum": ["pending", "confirmed"] }
                }
              },
              "example": { "name": "Ada Lovelace", "phone": "+15550100", "party_size": 2, "date": "2026-10-03", "time": "19:30" }
            }
          }
        },
        "responses": {
          "201": { "$ref": "#/components/responses/ReservationResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/availability": {
      "get": {
        "tags": ["Reservations"],
        "operationId": "getAvailability",
        "summary": "Bookable times for a day and a party",
        "x-scope": "reservations:read",
        "parameters": [
          { "name": "date", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "party_size", "in": "query", "required": true, "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": {
            "description": "The day's slots.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Availability" },
                    "meta": { "$ref": "#/components/schemas/NoLinks" },
                    "links": { "$ref": "#/components/schemas/NoLinks" }
                  }
                },
                "example": {
                  "data": { "date": "2026-10-03", "party_size": 2, "engine": true, "slots": [{ "time": "19:00", "available": true, "guarantee": false }, { "time": "19:30", "available": false, "guarantee": false }], "guarantee": null, "notify": true },
                  "meta": {},
                  "links": {}
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/reservations/{reservation}": {
      "get": {
        "tags": ["Reservations"],
        "operationId": "getReservation",
        "summary": "One booking",
        "x-scope": "reservations:read",
        "parameters": [{ "$ref": "#/components/parameters/ReservationId" }],
        "responses": {
          "200": { "$ref": "#/components/responses/ReservationResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/reservations/{reservation}/cancel": {
      "post": {
        "tags": ["Reservations"],
        "operationId": "cancelReservation",
        "summary": "Cancel a booking",
        "description": "Cancelled by the restaurant; the guest is told.",
        "x-scope": "reservations:write",
        "parameters": [{ "$ref": "#/components/parameters/ReservationId" }, { "$ref": "#/components/parameters/IdempotencyKey" }],
        "responses": {
          "200": { "$ref": "#/components/responses/ReservationResponse" },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/guests": {
      "get": {
        "tags": ["Guests"],
        "operationId": "listGuests",
        "summary": "Guest profiles, a page at a time",
        "description": "Guests who asked to be forgotten never appear. Phone and email are included only for guests who agreed to hear from the restaurant. Staff notes are never included.",
        "x-scope": "guests:read",
        "parameters": [
          { "name": "marketing_opted_in", "in": "query", "required": false, "schema": { "type": "string", "enum": ["true", "false", "1", "0"] } },
          { "name": "updated_since", "in": "query", "required": false, "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/PerPage" },
          { "$ref": "#/components/parameters/Cursor" }
        ],
        "responses": {
          "200": {
            "description": "A page of guests.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Guest" } },
                    "meta": { "$ref": "#/components/schemas/PageMeta" },
                    "links": { "$ref": "#/components/schemas/PageLinks" }
                  }
                },
                "example": {
                  "data": [{
                    "id": 5, "name": "Ada Lovelace", "phone": "+15550100", "email": "ada@example.com", "contact_shared": true, "locale": "en",
                    "marketing_opted_in": true, "marketing_opted_in_at": "2026-09-01T12:00:00+00:00", "visits_count": 4, "total_spend_minor": 9200,
                    "last_visit_at": "2026-09-28T20:15:00+00:00", "created_at": "2026-06-02T19:40:00+00:00"
                  }],
                  "meta": { "per_page": 25, "next_cursor": null, "prev_cursor": null },
                  "links": { "next": null, "prev": null }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/guests/{guest}": {
      "get": {
        "tags": ["Guests"],
        "operationId": "getGuest",
        "summary": "One guest profile",
        "x-scope": "guests:read",
        "parameters": [{ "name": "guest", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": {
            "description": "The guest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": { "data": { "$ref": "#/components/schemas/Guest" }, "meta": { "$ref": "#/components/schemas/NoLinks" }, "links": { "$ref": "#/components/schemas/NoLinks" } }
                },
                "example": {
                  "data": {
                    "id": 6, "name": "Alan Turing", "phone": null, "email": null, "contact_shared": false, "locale": null, "marketing_opted_in": false,
                    "marketing_opted_in_at": null, "visits_count": 1, "total_spend_minor": 2400, "last_visit_at": null, "created_at": "2026-09-30T11:00:00+00:00"
                  },
                  "meta": {},
                  "links": {}
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/reports/sales-summary": {
      "get": {
        "tags": ["Reports"],
        "operationId": "getSalesSummary",
        "summary": "Revenue, orders and average order for a period",
        "description": "With the preceding period of the same length. Defaults to today on the restaurant's clock; at most 366 days.",
        "x-scope": "reports:read",
        "parameters": [
          { "name": "from", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "to", "in": "query", "required": false, "schema": { "type": "string", "format": "date" } },
          { "name": "branch_id", "in": "query", "required": false, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": {
            "description": "The summary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "meta", "links"],
                  "properties": { "data": { "$ref": "#/components/schemas/SalesSummary" }, "meta": { "$ref": "#/components/schemas/NoLinks" }, "links": { "$ref": "#/components/schemas/NoLinks" } }
                },
                "example": {
                  "data": {
                    "from": "2026-09-01", "to": "2026-09-30", "branch_id": null, "currency": "EUR", "orders": 812, "revenue_minor": 1843200, "average_order_minor": 2270,
                    "previous": { "orders": 760, "revenue_minor": 1690000, "average_order_minor": 2223 },
                    "change": { "revenue_pct": 9.1, "orders_pct": 6.8 }
                  },
                  "meta": {},
                  "links": {}
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthenticated" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/oauth/token": {
      "servers": [{ "url": "/", "description": "This BookNDish host. The OAuth endpoints sit at its root, not under /api/v1." }],
      "post": {
        "operationId": "oauthToken",
        "tags": ["OAuth"],
        "summary": "Exchange a code, or refresh, for tokens",
        "description": "`authorization_code`: redeem the code the venue's consent sent to your redirect address, with the PKCE verifier. `refresh_token`: get a new access token and a new refresh token; the old pair stops working. Authenticate with your client id and secret, as HTTP Basic or as `client_id` and `client_secret` in the body. Answers are never cached.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/OAuthTokenRequest" } },
            "application/json": { "schema": { "$ref": "#/components/schemas/OAuthTokenRequest" }, "example": { "grant_type": "authorization_code", "code": "bnd_ac_9fK2...", "redirect_uri": "https://app.example.com/callback", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "client_id": "bnd_app_4kq8...", "client_secret": "bnd_cs_..." } }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OAuthTokens" }, "example": { "access_token": "812|bnd_oat_Xf3k...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "bnd_ort_q8Lz...", "scope": "menu:read orders:read" } } }
          },
          "400": {
            "$ref": "#/components/responses/OAuthBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/OAuthInvalidClient"
          },
          "429": {
            "$ref": "#/components/responses/OAuthRateLimited"
          }
        }
      }
    },
    "/oauth/revoke": {
      "servers": [{ "url": "/", "description": "This BookNDish host. The OAuth endpoints sit at its root, not under /api/v1." }],
      "post": {
        "operationId": "oauthRevoke",
        "tags": ["OAuth"],
        "summary": "Revoke a token",
        "description": "Gives back a refresh token (with its access token) or an access token (RFC 7009). An unknown token is not an error. Authenticate with your client id and secret, as HTTP Basic or as `client_id` and `client_secret` in the body. To end the whole grant, the venue uninstalls the app.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": { "schema": { "$ref": "#/components/schemas/OAuthRevokeRequest" } },
            "application/json": { "schema": { "$ref": "#/components/schemas/OAuthRevokeRequest" }, "example": { "token": "bnd_ort_q8Lz...", "client_id": "bnd_app_4kq8...", "client_secret": "bnd_cs_..." } }
          }
        },
        "responses": {
          "200": {
            "description": "Revoked, or already unknown.",
            "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false }, "example": {} } }
          },
          "400": {
            "$ref": "#/components/responses/OAuthBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/OAuthInvalidClient"
          },
          "429": {
            "$ref": "#/components/responses/OAuthRateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "event": {
      "post": {
        "summary": "Every webhook delivery",
        "description": "A `POST` to each address subscribed to the event, signed with `BookNDish-Signature: t=<unix time>,v1=<hex HMAC-SHA256 of \"{t}.{raw body}\" keyed with the endpoint's secret>`. `BookNDish-Event` names the event and `BookNDish-Delivery` carries the event id, the same on a retry or resend. Answer with any 2xx within 10 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Deliveries may arrive more than once and out of order: deduplicate on `id`. `ping` is the test event sent from the Developers screen.",
        "parameters": [
          { "name": "BookNDish-Event", "in": "header", "required": true, "schema": { "$ref": "#/components/schemas/WebhookEventType" } },
          { "name": "BookNDish-Delivery", "in": "header", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "BookNDish-Signature", "in": "header", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/WebhookEnvelope" },
              "example": {
                "id": "1f0c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
                "type": "stock.below_par",
                "created_at": "2026-09-29T10:15:02+00:00",
                "restaurant": { "id": 12, "slug": "trattoria", "currency": "EUR" },
                "data": { "stock": { "ingredient": { "id": 4, "name": "Tomatoes", "sku": "TOM-1", "unit": "kg" }, "branch_id": 1, "on_hand": 4, "par_level": 5, "previous_on_hand": 6 } }
              }
            }
          }
        },
        "responses": { "200": { "description": "Any 2xx marks the delivery done." } }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "A restaurant's API token, `<id>|bnd_...` (or `<id>|bnd_sbx_...` for a sandbox), created on Settings > Developers. Each operation names the scope it needs in `x-scope`." },
      "oauth2": {
        "type": "oauth2",
        "description": "For apps registered in the marketplace (Settings > Developers > Apps) and installed by a venue. The access token is a bearer token for this API, valid one hour, limited to the scopes the venue granted; refresh it with the refresh token, which is replaced on every use. Send the owner to `/oauth/authorize` with `response_type=code`, your `client_id`, a registered `redirect_uri`, the `scope`s, a `state` and a PKCE `code_challenge` (`code_challenge_method=S256`).",
        "flows": {
          "authorizationCode": { "authorizationUrl": "/oauth/authorize", "tokenUrl": "/oauth/token", "refreshUrl": "/oauth/token", "scopes": { "menu:read": "Read the menu", "menu:write": "Change dishes, prices and sold out", "orders:read": "Read orders", "orders:write": "Place orders and change their status", "reservations:read": "Read bookings", "reservations:write": "Make and cancel bookings", "guests:read": "Read guest profiles", "reports:read": "Read sales reports" } }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 255 }, "description": "Any POST may carry one. A retry with the same key and body within 24 hours gets the first response back (`Idempotent-Replayed: true`)." },
      "PerPage": { "name": "per_page", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } },
      "Cursor": { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "From `meta.next_cursor`, or follow `links.next`." },
      "MenuId": { "name": "menu", "in": "path", "required": true, "schema": { "type": "integer" } },
      "ItemId": { "name": "item", "in": "path", "required": true, "schema": { "type": "integer" } },
      "OrderId": { "name": "order", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "ReservationId": { "name": "reservation", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
    },
    "responses": {
      "ItemResponse": {
        "description": "The dish.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["data", "meta", "links"],
              "properties": {
                "data": { "$ref": "#/components/schemas/Item" },
                "meta": { "type": "object", "required": ["currency"], "properties": { "currency": { "$ref": "#/components/schemas/Currency" } } },
                "links": { "$ref": "#/components/schemas/NoLinks" }
              }
            },
            "example": {
              "data": {
                "id": 42, "category_id": 7, "name": { "en": "Burger" }, "description": { "en": "Beef, brioche" }, "price_minor": 1100, "is_available": true, "show_online": true,
                "dietary": "none", "allergens": ["gluten", "milk"], "sort_order": 1, "variants": [],
                "modifier_groups": [{ "id": 5, "name": { "en": "Extras" }, "min_select": 0, "max_select": null, "options": [{ "id": 9, "name": { "en": "Cheese" }, "price_minor": 100, "is_available": true }] }],
                "updated_at": "2026-09-29T10:12:00+00:00"
              },
              "meta": { "currency": "USD" },
              "links": {}
            }
          }
        }
      },
      "OrderResponse": {
        "description": "The order.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["data", "meta", "links"],
              "properties": { "data": { "$ref": "#/components/schemas/Order" }, "meta": { "$ref": "#/components/schemas/NoLinks" }, "links": { "$ref": "#/components/schemas/NoLinks" } }
            },
            "example": {
              "data": {
                "id": "9d1c7a52-8f0e-4b8c-a1f5-3a1a5b1f0c2d", "status": "placed", "type": "delivery", "channel": "web", "branch_id": 1, "table": null,
                "customer": { "name": "Ada Lovelace", "phone": "+15550100", "email": "ada@example.com" }, "note": "Ring twice",
                "delivery_address": { "line1": "12 Main St", "city": "Springfield" }, "currency": "USD",
                "subtotal_minor": 2300, "discount_minor": 0, "service_charge_minor": 0, "tax_added_minor": 0, "delivery_fee_minor": 300, "total_minor": 2600,
                "tax_breakdown": null, "cancel_reason": null,
                "items": [{ "id": 1, "item_id": 42, "name": { "en": "Burger" }, "variant_name": null, "quantity": 2, "unit_price_minor": 1200, "line_total_minor": 2400, "voided": false, "modifiers": [{ "name": { "en": "Cheese" }, "price_minor": 100 }] }],
                "placed_at": "2026-09-29T10:15:00+00:00", "scheduled_for": null, "paid_at": null, "updated_at": "2026-09-29T10:15:00+00:00"
              },
              "meta": {},
              "links": {}
            }
          }
        }
      },
      "ReservationResponse": {
        "description": "The booking.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": ["data", "meta", "links"],
              "properties": { "data": { "$ref": "#/components/schemas/Reservation" }, "meta": { "$ref": "#/components/schemas/NoLinks" }, "links": { "$ref": "#/components/schemas/NoLinks" } }
            },
            "example": {
              "data": {
                "id": "1b6e1c1e-2f4f-4c43-9e0e-4b9d6f1a2c3d", "status": "confirmed", "name": "Ada Lovelace", "phone": "+15550100", "email": null, "party_size": 2,
                "date": "2026-10-03", "time": "19:30", "table_id": null, "note": null, "deposit_minor": null, "deposit_status": null,
                "created_at": "2026-10-01T09:00:00+00:00", "updated_at": "2026-10-01T09:00:00+00:00"
              },
              "meta": {},
              "links": {}
            }
          }
        }
      },
      "Unauthenticated": { "description": "Missing, unknown, revoked or expired token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "unauthenticated", "message": "A valid API token is required.", "details": {} } } } } },
      "Forbidden": { "description": "The token lacks the scope (`insufficient_scope`), the plan lacks the feature (`plan_feature_unavailable`), a write while the restaurant is suspended (`restaurant_suspended`), or any call while it has no plan in force (`plan_lapsed`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "insufficient_scope", "message": "This token does not have the scope this endpoint needs.", "details": { "required_scope": "orders:write" } } } } } },
      "NotFound": { "description": "No such resource for this restaurant.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "not_found", "message": "Not found.", "details": {} } } } } },
      "Conflict": { "description": "An order or booking cannot move to that status from where it is (`invalid_transition`), or the first request with this Idempotency-Key is still running (`idempotency_key_in_use`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "invalid_transition", "message": "The order cannot move to that status.", "details": {} } } } } },
      "ValidationFailed": { "description": "Invalid fields (`validation_failed`, with `details` mapping each field to its messages), or an Idempotency-Key reused with another body (`idempotency_key_reused`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "validation_failed", "message": "The request has invalid fields.", "details": { "lines.0.item_id": ["The selected lines.0.item_id is invalid."] } } } } } },
      "RateLimited": { "description": "Too many requests for this token; wait `Retry-After` seconds.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "rate_limited", "message": "Too many requests.", "details": {} } } } } },
      "OAuthBadRequest": { "description": "The request or the grant is not valid (RFC 6749 section 5.2).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OAuthError" }, "example": { "error": "invalid_grant", "error_description": "The code_verifier does not match the code_challenge." } } } },
      "OAuthInvalidClient": { "description": "The client id or secret is wrong.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OAuthError" }, "example": { "error": "invalid_client", "error_description": "The client could not be authenticated." } } } },
      "OAuthRateLimited": { "description": "Too many requests from this address; wait `Retry-After` seconds.", "content": { "application/json": { "schema": { "type": "object", "required": ["message"], "properties": { "message": { "type": "string" } } }, "example": { "message": "Too Many Attempts." } } } }
    },
    "schemas": {
      "Currency": { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO 4217." },
      "Translations": { "type": "object", "description": "Text by language code.", "additionalProperties": { "type": ["string", "null"] } },
      "TextInput": { "oneOf": [{ "type": "string" }, { "$ref": "#/components/schemas/Translations" }] },
      "Dietary": { "type": "string", "enum": ["none", "veg", "non_veg", "egg", "vegan"] },
      "Allergen": { "type": "string", "enum": ["gluten", "crustaceans", "eggs", "fish", "peanuts", "soybeans", "milk", "nuts", "celery", "mustard", "sesame", "sulphites", "lupin", "molluscs"] },
      "OrderType": { "type": "string", "enum": ["dine_in", "pickup", "delivery"] },
      "OrderStatus": { "type": "string", "enum": ["placed", "accepted", "rejected", "ready", "completed", "cancelled"] },
      "ReservationStatus": { "type": "string", "enum": ["pending", "confirmed", "declined", "cancelled", "seated", "no_show"] },
      "NoLinks": { "type": "object", "description": "Empty here." },
      "PageMeta": {
        "type": "object",
        "required": ["per_page", "next_cursor", "prev_cursor"],
        "properties": { "per_page": { "type": "integer" }, "next_cursor": { "type": ["string", "null"] }, "prev_cursor": { "type": ["string", "null"] } }
      },
      "PageMetaWithCurrency": {
        "type": "object",
        "required": ["currency", "per_page", "next_cursor", "prev_cursor"],
        "properties": { "currency": { "$ref": "#/components/schemas/Currency" }, "per_page": { "type": "integer" }, "next_cursor": { "type": ["string", "null"] }, "prev_cursor": { "type": ["string", "null"] } }
      },
      "PageLinks": {
        "type": "object",
        "required": ["next", "prev"],
        "properties": { "next": { "type": ["string", "null"] }, "prev": { "type": ["string", "null"] } }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "details"],
            "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "details": { "type": "object", "additionalProperties": true } }
          }
        }
      },
      "Menu": {
        "type": "object",
        "required": ["id", "name", "is_active", "sort_order"],
        "properties": { "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "is_active": { "type": "boolean" }, "sort_order": { "type": "integer" } }
      },
      "MenuWithCategories": {
        "type": "object",
        "required": ["id", "name", "is_active", "sort_order", "categories"],
        "properties": {
          "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "is_active": { "type": "boolean" }, "sort_order": { "type": "integer" },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } }
        }
      },
      "MenuWithItems": {
        "type": "object",
        "required": ["id", "name", "is_active", "sort_order", "categories"],
        "properties": {
          "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "is_active": { "type": "boolean" }, "sort_order": { "type": "integer" },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/CategoryWithItems" } }
        }
      },
      "Category": {
        "type": "object",
        "required": ["id", "menu_id", "name", "is_active", "sort_order"],
        "properties": { "id": { "type": "integer" }, "menu_id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "is_active": { "type": "boolean" }, "sort_order": { "type": "integer" } }
      },
      "CategoryWithItems": {
        "type": "object",
        "required": ["id", "menu_id", "name", "is_active", "sort_order", "items"],
        "properties": {
          "id": { "type": "integer" }, "menu_id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "is_active": { "type": "boolean" }, "sort_order": { "type": "integer" },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } }
        }
      },
      "Item": {
        "type": "object",
        "required": ["id", "category_id", "name", "description", "price_minor", "is_available", "show_online", "dietary", "allergens", "sort_order", "variants", "modifier_groups", "updated_at"],
        "properties": {
          "id": { "type": "integer" },
          "category_id": { "type": "integer" },
          "name": { "$ref": "#/components/schemas/Translations" },
          "description": { "oneOf": [{ "$ref": "#/components/schemas/Translations" }, { "type": "null" }] },
          "price_minor": { "type": ["integer", "null"], "description": "Null for a dish priced per size (variants)." },
          "is_available": { "type": "boolean" },
          "show_online": { "type": "boolean" },
          "dietary": { "$ref": "#/components/schemas/Dietary" },
          "allergens": { "type": ["array", "null"], "items": { "$ref": "#/components/schemas/Allergen" } },
          "sort_order": { "type": "integer" },
          "variants": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "name", "price_minor", "is_default"],
              "properties": { "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "price_minor": { "type": "integer" }, "is_default": { "type": "boolean" } }
            }
          },
          "modifier_groups": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "name", "min_select", "max_select", "options"],
              "properties": {
                "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "min_select": { "type": "integer" }, "max_select": { "type": ["integer", "null"] },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": ["id", "name", "price_minor", "is_available"],
                    "properties": { "id": { "type": "integer" }, "name": { "$ref": "#/components/schemas/Translations" }, "price_minor": { "type": "integer" }, "is_available": { "type": "boolean" } }
                  }
                }
              }
            }
          },
          "updated_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Order": {
        "type": "object",
        "required": ["id", "status", "type", "channel", "branch_id", "table", "customer", "note", "delivery_address", "currency", "subtotal_minor", "discount_minor", "service_charge_minor", "tax_added_minor", "delivery_fee_minor", "total_minor", "tax_breakdown", "cancel_reason", "items", "placed_at", "scheduled_for", "paid_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/OrderStatus" },
          "type": { "$ref": "#/components/schemas/OrderType" },
          "channel": { "type": "string", "description": "web, phone, whatsapp, qr, pos, kiosk, or a delivery marketplace." },
          "branch_id": { "type": ["integer", "null"] },
          "table": {
            "type": ["object", "null"],
            "required": ["id", "name", "code"],
            "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "code": { "type": ["string", "null"] } }
          },
          "customer": {
            "type": "object",
            "required": ["name", "phone", "email"],
            "properties": { "name": { "type": ["string", "null"] }, "phone": { "type": ["string", "null"] }, "email": { "type": ["string", "null"] } }
          },
          "note": { "type": ["string", "null"] },
          "delivery_address": { "type": ["object", "null"], "additionalProperties": true },
          "currency": { "$ref": "#/components/schemas/Currency" },
          "subtotal_minor": { "type": "integer" },
          "discount_minor": { "type": "integer" },
          "service_charge_minor": { "type": "integer" },
          "tax_added_minor": { "type": "integer" },
          "delivery_fee_minor": { "type": "integer" },
          "total_minor": { "type": "integer" },
          "tax_breakdown": { "description": "Tax per rate where the venue's tax mode records it.", "type": ["array", "object", "null"] },
          "cancel_reason": { "type": ["string", "null"] },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "item_id", "name", "variant_name", "quantity", "unit_price_minor", "line_total_minor", "voided", "modifiers"],
              "properties": {
                "id": { "type": "integer" },
                "item_id": { "type": ["integer", "null"] },
                "name": { "$ref": "#/components/schemas/Translations" },
                "variant_name": { "oneOf": [{ "$ref": "#/components/schemas/Translations" }, { "type": "null" }] },
                "quantity": { "type": "integer" },
                "unit_price_minor": { "type": "integer" },
                "line_total_minor": { "type": "integer" },
                "voided": { "type": "boolean" },
                "modifiers": {
                  "type": "array",
                  "items": { "type": "object", "required": ["name", "price_minor"], "properties": { "name": { "$ref": "#/components/schemas/Translations" }, "price_minor": { "type": "integer" } } }
                }
              }
            }
          },
          "placed_at": { "type": ["string", "null"], "format": "date-time" },
          "scheduled_for": { "type": ["string", "null"], "format": "date-time" },
          "paid_at": { "type": ["string", "null"], "format": "date-time" },
          "updated_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Reservation": {
        "type": "object",
        "required": ["id", "status", "name", "phone", "email", "party_size", "date", "time", "table_id", "note", "deposit_minor", "deposit_status", "created_at", "updated_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": { "$ref": "#/components/schemas/ReservationStatus" },
          "name": { "type": "string" },
          "phone": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"] },
          "party_size": { "type": "integer" },
          "date": { "type": "string", "format": "date", "description": "The restaurant's own calendar, as the guest booked." },
          "time": { "type": "string", "description": "HH:MM on the restaurant's clock." },
          "table_id": { "type": ["integer", "null"] },
          "note": { "type": ["string", "null"] },
          "deposit_minor": { "type": ["integer", "null"] },
          "deposit_status": { "type": ["string", "null"], "enum": ["pending", "paid", "forfeited", "refunded", "waived", null] },
          "created_at": { "type": ["string", "null"], "format": "date-time" },
          "updated_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "Availability": {
        "type": "object",
        "required": ["date", "party_size", "engine", "slots", "guarantee", "notify"],
        "properties": {
          "date": { "type": "string", "format": "date" },
          "party_size": { "type": "integer" },
          "engine": { "type": "boolean", "description": "Whether the table availability engine answered (true), or the venue's fixed booking times did (false)." },
          "slots": {
            "type": "array",
            "items": { "type": "object", "required": ["time", "available", "guarantee"], "properties": { "time": { "type": "string" }, "available": { "type": "boolean" }, "guarantee": { "type": "boolean" } } }
          },
          "guarantee": {
            "type": ["object", "null"],
            "required": ["fee_minor", "cancel_hours"],
            "properties": { "fee_minor": { "type": "integer" }, "cancel_hours": { "type": ["integer", "null"] } }
          },
          "notify": { "type": "boolean", "description": "Whether a guest may ask to be told when a full time frees up." }
        }
      },
      "Guest": {
        "type": "object",
        "required": ["id", "name", "phone", "email", "contact_shared", "locale", "marketing_opted_in", "marketing_opted_in_at", "visits_count", "total_spend_minor", "last_visit_at", "created_at"],
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"], "description": "Only for a guest who agreed to share it." },
          "email": { "type": ["string", "null"], "description": "Only for a guest who agreed to share it." },
          "contact_shared": { "type": "boolean" },
          "locale": { "type": ["string", "null"] },
          "marketing_opted_in": { "type": "boolean" },
          "marketing_opted_in_at": { "type": ["string", "null"], "format": "date-time" },
          "visits_count": { "type": "integer" },
          "total_spend_minor": { "type": "integer" },
          "last_visit_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "SalesFigures": {
        "type": "object",
        "required": ["orders", "revenue_minor", "average_order_minor"],
        "properties": { "orders": { "type": "integer" }, "revenue_minor": { "type": "integer" }, "average_order_minor": { "type": "integer" } }
      },
      "SalesSummary": {
        "type": "object",
        "required": ["from", "to", "branch_id", "currency", "orders", "revenue_minor", "average_order_minor", "previous", "change"],
        "properties": {
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "branch_id": { "type": ["integer", "null"] },
          "currency": { "$ref": "#/components/schemas/Currency" },
          "orders": { "type": "integer" },
          "revenue_minor": { "type": "integer" },
          "average_order_minor": { "type": "integer" },
          "previous": { "$ref": "#/components/schemas/SalesFigures" },
          "unpaid_completed": {
            "description": "Completed orders in the range with no payment recorded. Null where the venue takes payments on its own register.",
            "type": ["object", "null"],
            "required": ["orders", "amount_minor"],
            "properties": { "orders": { "type": "integer" }, "amount_minor": { "type": "integer" } }
          },
          "change": {
            "type": "object",
            "required": ["revenue_pct", "orders_pct"],
            "properties": { "revenue_pct": { "type": ["number", "null"] }, "orders_pct": { "type": ["number", "null"] } }
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": ["order.created", "order.status_changed", "order.paid", "reservation.created", "reservation.updated", "reservation.cancelled", "menu.item_updated", "payment.refunded", "guest.created", "guest.updated", "giftcard.issued", "giftcard.redeemed", "review.created", "stock.below_par", "shift.published", "experience.booked", "ping"]
      },
      "WebhookEnvelope": {
        "type": "object",
        "required": ["id", "type", "created_at", "restaurant", "data"],
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "The event id, the same on every delivery of the event." },
          "type": { "$ref": "#/components/schemas/WebhookEventType" },
          "created_at": { "type": "string", "format": "date-time" },
          "restaurant": {
            "type": "object",
            "required": ["id", "slug", "currency"],
            "properties": { "id": { "type": "integer" }, "slug": { "type": "string" }, "currency": { "$ref": "#/components/schemas/Currency" } }
          },
          "data": { "type": "object", "additionalProperties": true, "description": "The event's subject in the same shape as the API returns it: `order`, `reservation`, `item`, `guest`, `gift_card` with `transaction`, `review`, `stock`, `shifts`, `booking`." }
        }
      },
      "OAuthTokens": {
        "type": "object",
        "required": ["access_token", "token_type", "expires_in", "refresh_token", "scope"],
        "additionalProperties": false,
        "properties": { "access_token": { "type": "string", "description": "The bearer token for this API, `<id>|bnd_oat_...`." }, "token_type": { "type": "string", "enum": ["Bearer"] }, "expires_in": { "type": "integer", "description": "Seconds the access token lives: 3600." }, "refresh_token": { "type": "string", "description": "`bnd_ort_...`, valid 30 days, replaced on every use. A refresh token used twice revokes the whole grant." }, "scope": { "type": "string", "description": "The scopes granted, space-separated." } }
      },
      "OAuthError": {
        "type": "object",
        "required": ["error", "error_description"],
        "additionalProperties": false,
        "properties": { "error": { "type": "string", "enum": ["invalid_request", "invalid_client", "invalid_grant", "unsupported_grant_type"] }, "error_description": { "type": "string" } }
      },
      "OAuthTokenRequest": {
        "type": "object",
        "required": ["grant_type"],
        "properties": { "grant_type": { "type": "string", "enum": ["authorization_code", "refresh_token"] }, "code": { "type": "string", "description": "For `authorization_code`: the code from the redirect, single use, valid five minutes." }, "redirect_uri": { "type": "string", "description": "For `authorization_code`: the same address the code was sent to." }, "code_verifier": { "type": "string", "description": "For `authorization_code`: the PKCE verifier whose S256 hash was the `code_challenge`. Not sent for a code from the listing's Install button." }, "refresh_token": { "type": "string", "description": "For `refresh_token`." }, "client_id": { "type": "string", "description": "Unless sent as HTTP Basic." }, "client_secret": { "type": "string", "description": "Unless sent as HTTP Basic." } }
      },
      "OAuthRevokeRequest": {
        "type": "object",
        "required": ["token"],
        "properties": { "token": { "type": "string", "description": "A refresh token (its access token is revoked with it) or an access token." }, "client_id": { "type": "string" }, "client_secret": { "type": "string" } }
      }
    }
  }
}
