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=,v1=`. `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, `|bnd_...` (or `|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, `|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"