{
  "openapi": "3.0.3",
  "info": { "title": "Servatto Menu API", "version": "1.0.0", "description": "Manage a Servatto venue's menu: items, sizes and prices, modifier sets, categories, sold-out per location. Money is in cents. Authenticate with an API key from the back office (Items & menus → API access): Authorization: Bearer svk_… The key manages the menu only." },
  "servers": [{ "url": "https://app.servatto.com/api/office" }],
  "components": {
    "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer", "bearerFormat": "svk_…" } },
    "schemas": {
      "Variation": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "priceCents": { "type": "integer", "nullable": true, "description": "null = variable price typed at the till" }, "sku": { "type": "string", "nullable": true }, "upc": { "type": "string", "nullable": true }, "soldOut": { "type": "boolean" } }, "required": ["name"] },
      "ItemModifierList": { "type": "object", "properties": { "id": { "type": "string" }, "minSelected": { "type": "integer" }, "maxSelected": { "type": "integer", "description": "-1 = no limit" } }, "required": ["id"] },
      "Item": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "description": { "type": "string", "nullable": true }, "color": { "type": "string", "nullable": true, "description": "RRGGBB tile colour when there is no photo" }, "imageUrl": { "type": "string", "nullable": true }, "gstFree": { "type": "boolean" }, "ordinal": { "type": "integer" }, "categoryIds": { "type": "array", "items": { "type": "string" } }, "modifierLists": { "type": "array", "items": { "$ref": "#/components/schemas/ItemModifierList" } }, "variations": { "type": "array", "items": { "$ref": "#/components/schemas/Variation" } }, "soldOut": { "type": "boolean" } } },
      "ItemWrite": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" }, "color": { "type": "string" }, "imageUrl": { "type": "string" }, "gstFree": { "type": "boolean" }, "categoryIds": { "type": "array", "items": { "type": "string" } }, "modifierLists": { "type": "array", "items": { "$ref": "#/components/schemas/ItemModifierList" } }, "variations": { "type": "array", "items": { "$ref": "#/components/schemas/Variation" }, "description": "Replaces the sizes; include id to keep a size" } } },
      "Category": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "parentId": { "type": "string", "nullable": true }, "isMenu": { "type": "boolean", "description": "A top-level menu (Dine In, Takeaway…) holding categories" }, "imageUrl": { "type": "string", "nullable": true }, "ordinal": { "type": "integer" } } },
      "Modifier": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "priceCents": { "type": "integer" }, "onByDefault": { "type": "boolean" }, "soldOut": { "type": "boolean" } }, "required": ["name"] },
      "ModifierList": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "selectionType": { "type": "string", "enum": ["SINGLE", "MULTIPLE"] }, "minSelected": { "type": "integer" }, "maxSelected": { "type": "integer", "description": "-1 = no limit" }, "modifiers": { "type": "array", "items": { "$ref": "#/components/schemas/Modifier" } } } },
      "Menu": { "type": "object", "properties": { "master": { "type": "string", "enum": ["square", "servatto"] }, "orders": { "type": "string", "enum": ["square", "servatto"] }, "importedAt": { "type": "string", "nullable": true }, "menu": { "type": "object", "properties": { "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/Item" } }, "modifierLists": { "type": "array", "items": { "$ref": "#/components/schemas/ModifierList" } } } } } },
      "Error": { "type": "object", "properties": { "error": { "type": "string" } } }
    }
  },
  "security": [{ "apiKey": [] }],
  "paths": {
    "/menu": { "get": { "summary": "The whole menu", "parameters": [{ "name": "venue", "in": "query", "schema": { "type": "string" }, "description": "Location id for sold-out flags (default: first location)" }], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Menu" } } } } } } },
    "/menu/preview": { "get": { "summary": "Exactly what a till at the location is served", "responses": { "200": { "description": "OK" } } } },
    "/menu/items": { "post": { "summary": "Add an item", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ItemWrite" } } } }, "responses": { "201": { "description": "Created" }, "400": { "description": "Bad input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Menu is read from Square" } } } },
    "/menu/items/{id}": {
      "get": { "summary": "One item", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "item": { "$ref": "#/components/schemas/Item" } } } } } }, "404": { "description": "Not found" } } },
      "patch": { "summary": "Change an item (only the fields sent)", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ItemWrite" } } } }, "responses": { "200": { "description": "OK" } } },
      "delete": { "summary": "Take an item off the menu (past sales keep it)", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } }
    },
    "/menu/items/{id}/duplicate": { "post": { "summary": "Copy an item", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "201": { "description": "Created" } } } },
    "/menu/categories": { "post": { "summary": "Add a category (or a menu)", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Category" } } } }, "responses": { "201": { "description": "Created" } } } },
    "/menu/categories/{id}": { "patch": { "summary": "Change a category", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } }, "delete": { "summary": "Remove a category (its items stay)", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } } },
    "/menu/modifier-lists": { "post": { "summary": "Add a modifier set", "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModifierList" } } } }, "responses": { "201": { "description": "Created" } } } },
    "/menu/modifier-lists/{id}": { "patch": { "summary": "Change a modifier set (sending modifiers replaces them; include id to keep one)", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } }, "delete": { "summary": "Remove a modifier set from every item", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } } },
    "/menu/order": { "put": { "summary": "The order things show in", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "string" } }, "categories": { "type": "array", "items": { "type": "string" } }, "modifierLists": { "type": "array", "items": { "type": "string" } } } } } } }, "responses": { "200": { "description": "OK" } } } },
    "/menu/sold-out": { "post": { "summary": "Sold out (or back on) at one location: an item, a size or a modifier", "parameters": [{ "name": "venue", "in": "query", "required": true, "schema": { "type": "string" } }], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "objectId": { "type": "string" }, "soldOut": { "type": "boolean" }, "duration": { "type": "string", "enum": ["today", "4h", "indefinite"], "description": "today = until the next 5 am at the location; no duration = until turned back on" }, "until": { "type": "string", "format": "date-time" }, "reason": { "type": "string" } }, "required": ["objectId"] } } } }, "responses": { "200": { "description": "OK" } } }, "get": { "summary": "Everything sold out now, per location", "parameters": [{ "name": "venueId", "in": "query", "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } } },
    "/menu/items/{id}/locations/{venueId}": { "put": { "summary": "An item at one location: its price, not sold here, sold out", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "venueId", "in": "path", "required": true, "schema": { "type": "string" } }], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "priceCents": { "type": ["integer", "null"], "minimum": 0, "maximum": 1000000, "description": "Every size; null = the base price" }, "sizes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "priceCents": { "type": ["integer", "null"] } }, "required": ["id"] } }, "absent": { "type": "boolean", "description": "true = not sold at this location" }, "soldOut": { "type": "boolean" }, "duration": { "type": "string", "enum": ["today", "4h", "indefinite"], "description": "today = until the next 5 am at the location; no duration = until turned back on" } } } } } }, "responses": { "200": { "description": "OK" } } } },
    "/menu/items/{id}/locations": { "get": { "summary": "The item at each location", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "OK" } } } },
    "/menu/modifiers/{id}/locations/{venueId}": { "put": { "summary": "A modifier choice sold out (or back on) at one location", "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "venueId", "in": "path", "required": true, "schema": { "type": "string" } }], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "soldOut": { "type": "boolean" }, "duration": { "type": "string", "enum": ["today", "4h", "indefinite"], "description": "today = until the next 5 am at the location; no duration = until turned back on" } }, "required": ["soldOut"] } } } }, "responses": { "200": { "description": "OK" } } } }
  }
}
