{
  "openapi": "3.1.0",
  "info": {
    "title": "Quiqqy Online Ordering API",
    "version": "2.0.0",
    "summary": "Connect a POS to Quiqqy-powered merchant storefronts: onboard stores, sync menus, and fulfil online orders.",
    "description": "The **Online Ordering API** is Quiqqy's POS integration surface. It is built for **POS companies and ordering-tech providers** who want their merchants' stores live on Quiqqy storefronts (`*.quiqqy.app`), with the POS remaining the source of truth for the catalogue and the fulfilment system for every order.\n\n## What you can do\n\n- **Onboard merchants** — provision a merchant account + store from your POS back office and hand the merchant a ready-made OAuth signup URL (`POST /pos/signup_url`).\n- **Connect stores** — link your app to each store (`POST /pos/integrate-store/{store_id}`), receiving a per-store `webhook_secret` for verifying event signatures.\n- **Sync menus** — push the full menu (menus → categories → items → modifier groups, all keyed on **your** `external_id`s), patch single items, batch-update availability (86ing), and poll asynchronous sync jobs.\n- **Fulfil orders** — receive `order.created` webhooks, then accept, progress, mark ready, or cancel with structured reason codes.\n- **Manage webhooks** — read and rotate the per-store callback URL.\n\n## Authentication\n\nAll `/pos/*` endpoints (except `POST /pos/signup_url`) require a **merchant access token** — an OAuth 2.0 Bearer JWT obtained through the authorization-code flow:\n\n1. `POST /pos/signup_url` (Basic auth: `client_id` / `client_secret`) returns a `signup_url`.\n2. The merchant authorizes your app and is redirected back with `?code=...`.\n3. Exchange the code at `POST /oauth2/token` for `{ access_token, refresh_token, expires_in }`.\n4. Call `/pos/*` with `Authorization: Bearer <access_token>`; refresh via `POST /oauth2/refresh`.\n\nThe token is minted from your `client_id`, so it already identifies your app — **never send `app_id` in request bodies**. API keys are not accepted on `/pos/*` routes.\n\n## Conventions\n\n- All monetary amounts are **integers in minor units** of the store currency (e.g. `1299` = $12.99).\n- All request/response fields are `snake_case`.\n- Ids are opaque strings (`st_...` stores, `ord_...` orders, `int_...` integrations). External ids (`external_id`) are **yours** — the stable POS/PLU ids you push, echoed back on every order line.\n- Errors use a single shape: `{ \"error\": { \"code\": \"order.not_found\", \"message\": \"...\" } }` with stable dot-namespaced codes.\n- Webhooks are signed with `X-Quiqqy-Signature` (HMAC-SHA256 hex of the raw body) and retried on a `1m / 5m / 30m / 2h / 6h` ladder (6 attempts total). See the **Webhooks** section of this reference.",
    "contact": {
      "name": "Quiqqy Developer Platform",
      "url": "https://developers.quiqqy.ai",
      "email": "developers@quiqqy.ai"
    },
    "termsOfService": "https://developers.quiqqy.ai/terms"
  },
  "servers": [
    {
      "url": "https://auth.quiqqy.ai/api/v2",
      "description": "Production"
    },
    {
      "url": "https://sandbox.quiqqy.ai/api/v2",
      "description": "Sandbox — test stores and test orders, isolated from production data"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Onboarding",
      "description": "Provision merchants and generate OAuth signup URLs."
    },
    {
      "name": "Stores",
      "description": "List merchant stores, push operational settings, toggle live/paused, and read tax configuration."
    },
    {
      "name": "Integrations",
      "description": "Connect your app to a store and manage the integration (webhook URL, menu sync direction, auto-accept)."
    },
    {
      "name": "Menus",
      "description": "Full menu pushes (sync or async with job polling), batch operations, and sync status."
    },
    {
      "name": "Items",
      "description": "Single-item updates and batch availability (86ing)."
    },
    {
      "name": "Tables",
      "description": "Sync the POS floor plan; each table gets a QR ordering token."
    },
    {
      "name": "Orders",
      "description": "Acknowledge and progress orders received via webhooks: accept, status, ready, cancel."
    },
    {
      "name": "Webhooks",
      "description": "Per-store webhook (callback URL) configuration. Event payloads are documented under Webhooks in this reference."
    }
  ],
  "paths": {
    "/pos/signup_url": {
      "post": {
        "operationId": "createSignupUrl",
        "tags": ["Onboarding"],
        "summary": "Create a merchant signup URL",
        "description": "Provisions (or finds) a merchant account and store from the details you pass, then returns a ready-made OAuth **authorize URL**. Redirect the merchant to `signup_url` so they can log in / sign up and authorize your app; they return to your `redirect_uri` with `?code=...`, which you exchange for tokens at `POST /oauth2/token`.\n\nThis is the only `/pos/*` endpoint that uses **Basic auth** — send your `client_id` as the username and `client_secret` as the password. Alternatively pass `client_id` in the body.",
        "security": [
          {
            "basicAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupUrlRequest"
              },
              "example": {
                "location_id": "LITHOS-LOC-001",
                "store_name": "Main Street Pizza",
                "email": "owner@mainstreetpizza.com",
                "first_name": "Jane",
                "last_name": "Doe",
                "business_phone": "+14155550142",
                "address": {
                  "street_address": "123 Main St",
                  "city": "New York",
                  "state": "NY",
                  "country": "US",
                  "zip": "10001"
                },
                "locale": "en-US",
                "redirect_uri": "https://pos.example.com/quiqqy/callback",
                "state": "csrf_8c1f2ab4",
                "flow_type": "signup"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signup URL created. Redirect the merchant to `signup_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignupUrlResponse"
                },
                "example": {
                  "status": "success",
                  "signup_url": "https://auth.quiqqy.ai/api/v2/oauth2/authorize?client_id=qc_live_7f31a9&redirect_uri=https%3A%2F%2Fpos.example.com%2Fquiqqy%2Fcallback&email=owner%40mainstreetpizza.com&state=csrf_8c1f2ab4"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "A merchant with this email already exists with a conflicting store mapping. Use `flow_type: \"login\"` to send the existing merchant through the login flow.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "onboarding.merchant_exists",
                    "message": "A merchant account already exists for owner@mainstreetpizza.com. Retry with flow_type=login."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores": {
      "get": {
        "operationId": "listStores",
        "tags": ["Stores"],
        "summary": "List stores",
        "description": "Lists every store in the merchant's organization together with its `integration_status` for **your** app (`not_integrated`, `integrated`, `disabled`, `pending`). When a store is integrated, the current integration configuration is embedded (the `webhook_secret` is never returned here — it is only shown once, on `POST /pos/integrate-store/{store_id}`).",
        "responses": {
          "200": {
            "description": "Stores in the merchant's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Store"
                  }
                },
                "example": [
                  {
                    "store_id": "st_9x2ml7qa41ce",
                    "name": "Main Street Pizza",
                    "description": null,
                    "status": "active",
                    "integration_status": "integrated",
                    "integration": {
                      "integration_id": "int_5rk20dq8xw3f",
                      "webhook_url": "https://pos.example.com/webhooks/quiqqy",
                      "menu_sync_direction": "pos_to_quiqqy",
                      "auto_accept_orders": false,
                      "external_store_id": "LITHOS-LOC-001",
                      "connected_at": "2026-05-14T12:00:00.000Z"
                    }
                  },
                  {
                    "store_id": "st_1bd83k2mv7pq",
                    "name": "Main Street Pizza — Brooklyn",
                    "description": "Second location",
                    "status": "active",
                    "integration_status": "not_integrated",
                    "integration": null
                  }
                ]
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/integrate-store/{store_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "post": {
        "operationId": "integrateStore",
        "tags": ["Integrations"],
        "summary": "Connect your app to a store",
        "description": "Creates the integration between your app and a store: webhook URL, menu sync direction, auto-accept behaviour, and your own `external_store_id` (echoed back on every webhook as `X-Quiqqy-Location-Id`).\n\nThe response contains the **`webhook_secret`** used to sign every webhook delivery for this store. It is returned **only here** — store it securely server-side. If you lose it, disconnect and re-integrate the store to rotate it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IntegrateStoreRequest"
              },
              "example": {
                "webhook_url": "https://pos.example.com/webhooks/quiqqy",
                "notification_email": "ops@pos.example.com",
                "menu_sync_direction": "pos_to_quiqqy",
                "external_store_id": "LITHOS-LOC-001",
                "auto_accept_orders": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Store connected. Persist `webhook_secret` now — it is not returned again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Integration"
                },
                "example": {
                  "integration_id": "int_5rk20dq8xw3f",
                  "app_id": "app_2c9d7fk1qz84",
                  "store_id": "st_9x2ml7qa41ce",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy",
                  "webhook_secret": "whsec_4f7a1e0b9c2d8e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f",
                  "menu_sync_direction": "pos_to_quiqqy",
                  "auto_accept_orders": false,
                  "external_store_id": "LITHOS-LOC-001",
                  "notification_email": "ops@pos.example.com",
                  "status": "active",
                  "connected_at": "2026-05-14T12:00:00.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "The store is already integrated with your app. Use `PUT /pos/integrations/{store_id}` to change settings.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "integration.already_exists",
                    "message": "Store st_9x2ml7qa41ce is already integrated with this app."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/integrations/{store_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "get": {
        "operationId": "getIntegration",
        "tags": ["Integrations"],
        "summary": "Get integration configuration",
        "description": "Returns the current integration configuration for a store: webhook URL, menu sync direction, auto-accept behaviour, and connection status. The `webhook_secret` is never returned by this endpoint.",
        "responses": {
          "200": {
            "description": "Current integration configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Integration"
                },
                "example": {
                  "integration_id": "int_5rk20dq8xw3f",
                  "app_id": "app_2c9d7fk1qz84",
                  "store_id": "st_9x2ml7qa41ce",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy",
                  "menu_sync_direction": "pos_to_quiqqy",
                  "auto_accept_orders": false,
                  "external_store_id": "LITHOS-LOC-001",
                  "notification_email": "ops@pos.example.com",
                  "status": "active",
                  "connected_at": "2026-05-14T12:00:00.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "put": {
        "operationId": "updateIntegration",
        "tags": ["Integrations"],
        "summary": "Update integration settings",
        "description": "Updates the integration's webhook URL, notification email, menu sync direction, or auto-accept behaviour. Only the fields you send are changed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateIntegrationRequest"
              },
              "example": {
                "webhook_url": "https://pos.example.com/webhooks/quiqqy/v2",
                "menu_sync_direction": "bidirectional",
                "auto_accept_orders": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated integration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Integration"
                },
                "example": {
                  "integration_id": "int_5rk20dq8xw3f",
                  "app_id": "app_2c9d7fk1qz84",
                  "store_id": "st_9x2ml7qa41ce",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy/v2",
                  "menu_sync_direction": "bidirectional",
                  "auto_accept_orders": true,
                  "external_store_id": "LITHOS-LOC-001",
                  "notification_email": "ops@pos.example.com",
                  "status": "active",
                  "connected_at": "2026-05-14T12:00:00.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "delete": {
        "operationId": "deleteIntegration",
        "tags": ["Integrations"],
        "summary": "Disconnect a store",
        "description": "Disconnects your app from the store. Webhook deliveries stop, menu pushes are rejected, and the store's `integration_status` returns to `not_integrated`. Re-integrating issues a **new** `webhook_secret`.",
        "responses": {
          "200": {
            "description": "Integration removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SimpleStatusResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Integration disconnected"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/settings": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "put": {
        "operationId": "updateStoreSettings",
        "tags": ["Stores"],
        "summary": "Push store settings",
        "description": "Pushes operational store settings from the POS: display name, description, image, address, opening hours (`availability`), delivery settings, contact details, and feature toggles.\n\n> **Note:** field names on this endpoint mirror the merchant store-settings object and are **camelCase** (`imageUrl`, `deliverySettings`, ...) — the body is forwarded verbatim to the merchant catalogue service. This is the one exception to the snake_case convention.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StoreSettingsRequest"
              },
              "example": {
                "name": "Main Street Pizza",
                "description": "Neapolitan pizza, since 1998",
                "imageUrl": "https://cdn.quiqqy.ai/stores/main-street-pizza.jpg",
                "address": {
                  "street_address": "123 Main St",
                  "city": "New York",
                  "state": "NY",
                  "country": "US",
                  "zip": "10001"
                },
                "availability": {
                  "monday": [{ "open": "11:00", "close": "22:00" }],
                  "tuesday": [{ "open": "11:00", "close": "22:00" }]
                },
                "deliverySettings": {
                  "enabled": true,
                  "min_order_amount": 1500,
                  "delivery_fee": 200,
                  "radius_km": 5
                },
                "contactDetails": {
                  "phone": "+14155550142",
                  "email": "owner@mainstreetpizza.com"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings applied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SimpleStatusResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Store settings updated"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/status": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "put": {
        "operationId": "setStoreStatus",
        "tags": ["Stores"],
        "summary": "Set store live / paused",
        "description": "Toggles the store's online-ordering availability from the POS. `active` makes the storefront accept orders; `inactive` pauses it (the storefront shows the store as temporarily closed). A `store.status_changed` webhook is emitted to other subscribed channels.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetStoreStatusRequest"
              },
              "example": {
                "status": "inactive"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Store status updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "store_id", "store_status"],
                  "properties": {
                    "status": { "type": "string", "const": "success" },
                    "store_id": { "type": "string" },
                    "store_status": { "$ref": "#/components/schemas/StoreStatus" }
                  }
                },
                "example": {
                  "status": "success",
                  "store_id": "st_9x2ml7qa41ce",
                  "store_status": "inactive"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/tax-config": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "get": {
        "operationId": "getTaxConfig",
        "tags": ["Stores"],
        "summary": "Get tax configuration",
        "description": "Returns the store's POS-safe tax configuration so your menu push can declare taxes correctly: whether tax is enabled, at which level it applies (`store`, `category`, or `product`), whether displayed prices are tax-`inclusive` or `exclusive`, and the resolvable tax definitions with their components.",
        "responses": {
          "200": {
            "description": "Trimmed tax configuration for this store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaxConfig"
                },
                "example": {
                  "enabled": true,
                  "tax_level": "product",
                  "pricing_mode": "exclusive",
                  "default_tax_id": "tax_71h2cd0e9f3a",
                  "taxes": [
                    {
                      "tax_id": "tax_71h2cd0e9f3a",
                      "name": "NYC Sales Tax",
                      "components": [
                        { "agency": "NY State", "rate": 4, "compound": false },
                        { "agency": "NYC", "rate": 4.875, "compound": false }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/menus": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "post": {
        "operationId": "pushMenu",
        "tags": ["Menus"],
        "summary": "Push full menu",
        "description": "Pushes your full POS menu to Quiqqy. Requires an active integration with `menu_sync_direction` of `pos_to_quiqqy` or `bidirectional`.\n\nTwo body shapes are accepted:\n\n- **Preferred — `menus[]`**: one or more named menus, each with its own `categories`, `items`, `add_ons`, and `addon_groups`.\n- **Flat**: top-level `menu_name` + `items`/`categories`/`add_ons`/`addon_groups` for a single menu.\n\nEverything is keyed on **your** stable `external_id` (idempotency key) — re-pushing upserts. A dropped variant is deactivated; add-ons (`product_type: \"addon\"` or `add_ons[]`) never appear as standalone storefront products.\n\n**Sync vs async:** small menus (roughly under 100 items) are forwarded synchronously and return **200**. Larger menus are accepted with **202** and processed in the background — poll the returned `poll_url` (`GET /pos/stores/{store_id}/menus/jobs/{job_id}`) until the job status is `success` or `failed`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MenuPushRequest"
              },
              "example": {
                "menus": [
                  {
                    "external_id": "menu-main",
                    "name": "Main Menu",
                    "description": "All-day menu",
                    "categories": [
                      { "external_id": "cat-pizza", "name": "Pizza" },
                      { "external_id": "cat-drinks", "name": "Drinks" }
                    ],
                    "add_ons": [
                      { "external_id": "addon-xcheese", "name": "Extra cheese", "price": 150 },
                      { "external_id": "addon-olives", "name": "Olives", "price": 100 }
                    ],
                    "addon_groups": [
                      {
                        "external_id": "grp-toppings",
                        "name": "Toppings",
                        "min_select": 0,
                        "max_select": 3,
                        "required": false,
                        "add_ons": ["addon-xcheese", "addon-olives"]
                      }
                    ],
                    "items": [
                      {
                        "external_id": "SKU-MARG",
                        "name": "Margherita Pizza",
                        "description": "Tomato, mozzarella, basil",
                        "price": 1299,
                        "image_url": "https://cdn.pos.example.com/items/margherita.jpg",
                        "available": true,
                        "categories": ["cat-pizza"],
                        "addon_groups": ["grp-toppings"],
                        "variant_type": "stand_alone",
                        "variants": [
                          { "external_id": "SKU-MARG-SM", "variant_name": "Small", "price": 1299 },
                          { "external_id": "SKU-MARG-LG", "variant_name": "Large", "price": 1699 }
                        ],
                        "tax": {
                          "name": "NYC Sales Tax",
                          "components": [
                            { "agency": "NY State", "rate": 4 },
                            { "agency": "NYC", "rate": 4.875 }
                          ]
                        }
                      }
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Menu forwarded and persisted synchronously. `status` is `success`, or `partial` when the push was accepted but downstream forwarding reported an issue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuPushSyncResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Menu pushed to Quiqqy",
                  "data": {
                    "menus": 1,
                    "categories": 2,
                    "items": 1,
                    "add_ons": 2,
                    "addon_groups": 1
                  }
                }
              }
            }
          },
          "202": {
            "description": "Large menu accepted and queued. Poll `poll_url` until the job reaches a terminal state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuPushAsyncResponse"
                },
                "example": {
                  "status": "accepted",
                  "job_id": "job_8m2xk40qc7ra",
                  "message": "Menu push accepted; processing in background",
                  "poll_url": "/pos/stores/st_9x2ml7qa41ce/menus/jobs/job_8m2xk40qc7ra"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "Menu push not allowed for this store's sync direction (`quiqqy_to_pos` or `none`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "menu.push_not_allowed",
                    "message": "Integration sync direction quiqqy_to_pos does not permit POS menu pushes."
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": {
            "description": "Forwarding to the merchant catalogue service failed. Safe to retry with backoff.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "menu.forward_failed",
                    "message": "Menu received but forwarding to the catalogue service failed. Retry the push."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pos/stores/{store_id}/menus/jobs/{job_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" },
        { "$ref": "#/components/parameters/JobId" }
      ],
      "get": {
        "operationId": "getMenuSyncJob",
        "tags": ["Menus"],
        "summary": "Poll an async menu push job",
        "description": "Polls a background menu push accepted with **202**. Jobs are retained for **1 hour** after their last update; poll every few seconds until `status` is `success` or `failed`. On failure, fix the reported error and re-push the full menu.",
        "responses": {
          "200": {
            "description": "Current job state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuSyncJob"
                },
                "example": {
                  "job_id": "job_8m2xk40qc7ra",
                  "store_id": "st_9x2ml7qa41ce",
                  "status": "success",
                  "menus": 2,
                  "items": 342,
                  "created_at": "2026-07-30T09:15:02.000Z",
                  "updated_at": "2026-07-30T09:15:41.000Z",
                  "result": {
                    "categories": 18,
                    "items": 342,
                    "add_ons": 57,
                    "addon_groups": 12
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "Job not found or expired (jobs are retained for 1 hour).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "menu.job_not_found",
                    "message": "Menu sync job job_8m2xk40qc7ra was not found for store st_9x2ml7qa41ce."
                  }
                }
              }
            }
          },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/menus/status": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "get": {
        "operationId": "getMenuSyncStatus",
        "tags": ["Menus"],
        "summary": "Get menu sync status",
        "description": "Returns the store's menu sync configuration and the timestamp of the last successful sync.",
        "responses": {
          "200": {
            "description": "Sync status for this store.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuSyncStatus"
                },
                "example": {
                  "store_id": "st_9x2ml7qa41ce",
                  "menu_sync_direction": "pos_to_quiqqy",
                  "integration_status": "active",
                  "last_synced_at": "2026-07-30T09:15:41.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/menus/batch": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "post": {
        "operationId": "batchMenuOperations",
        "tags": ["Menus"],
        "summary": "Batch add / update / delete items",
        "description": "Applies a batch of catalogue changes in one call: `add` and `update` take full item objects (same shape as a menu-push item), `delete` takes a list of item `external_id`s. Optionally include `categories`, `add_ons`, and `addon_groups` definitions referenced by the added/updated items. Use this for incremental syncs; use the full menu push for a complete replace.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchMenuOperationRequest"
              },
              "example": {
                "add": [
                  {
                    "external_id": "SKU-CALZONE",
                    "name": "Calzone",
                    "price": 1149,
                    "categories": ["cat-pizza"],
                    "available": true
                  }
                ],
                "update": [
                  {
                    "external_id": "SKU-MARG",
                    "name": "Margherita Pizza",
                    "price": 1399
                  }
                ],
                "delete": ["SKU-HAWAII"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch applied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchOperationResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Batch applied",
                  "added": 1,
                  "updated": 1,
                  "deleted": 1
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/items/availability": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "post": {
        "operationId": "batchItemAvailability",
        "tags": ["Items"],
        "summary": "Batch update item availability (86ing)",
        "description": "Marks multiple items in or out of stock in one call — the real-time 86 endpoint. Items are addressed by **your** `external_id`. Out-of-stock items stay on the menu but cannot be ordered until re-enabled. Prefer this over a full menu push for stock changes; it is cheap and applies immediately to the storefront.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchAvailabilityRequest"
              },
              "example": {
                "items": [
                  { "external_id": "SKU-MARG", "available": false },
                  { "external_id": "SKU-CALZONE", "available": true }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Availability updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Availability updated",
                  "updated": 2,
                  "not_found": []
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/items/{ext_item_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" },
        { "$ref": "#/components/parameters/ExtItemId" }
      ],
      "patch": {
        "operationId": "updateItem",
        "tags": ["Items"],
        "summary": "Update a single item",
        "description": "Updates one catalogue item, addressed by **your** `external_id`. The body follows the menu item shape; send only the fields you want to change (plus the identifying `external_id` and required `name`/`price` when your POS resends the whole record).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MenuItem"
              },
              "example": {
                "external_id": "SKU-MARG",
                "name": "Margherita Pizza (Large)",
                "price": 1499,
                "available": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SimpleStatusResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Item updated"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "No item with this `external_id` was pushed by your app for this store.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "menu.item_not_found",
                    "message": "No item with external_id SKU-MARG exists for store st_9x2ml7qa41ce."
                  }
                }
              }
            }
          },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/tables": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "post": {
        "operationId": "pushTables",
        "tags": ["Tables"],
        "summary": "Sync tables (floor plan)",
        "description": "Syncs the POS floor plan to Quiqqy. Each table receives a stable **QR ordering token** — print it at the table so dine-in customers can order to that table. A re-push upserts by `external_id`; tables missing from the push are deactivated.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TablesRequest"
              },
              "example": {
                "tables": [
                  { "external_id": "T-01", "name": "Table 1", "seats": 4, "zone": "Main floor" },
                  { "external_id": "T-02", "name": "Table 2", "seats": 2, "zone": "Terrace" }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tables synced; QR tokens returned per table.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TablesResponse"
                },
                "example": {
                  "status": "success",
                  "tables": [
                    {
                      "external_id": "T-01",
                      "table_id": "tbl_3fq81mz2vk9c",
                      "qr_token": "qrt_c81d2e9f4a5b",
                      "qr_url": "https://mainstreetpizza.quiqqy.app/t/qrt_c81d2e9f4a5b"
                    },
                    {
                      "external_id": "T-02",
                      "table_id": "tbl_7wp42kd9xn1e",
                      "qr_token": "qrt_a95e7c2b8d3f",
                      "qr_url": "https://mainstreetpizza.quiqqy.app/t/qrt_a95e7c2b8d3f"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": {
            "description": "Forwarding to the merchant service failed. Safe to retry.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "tables.forward_failed",
                    "message": "Tables received but forwarding failed. Retry the push."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pos/orders/{order_id}/accept": {
      "parameters": [
        { "$ref": "#/components/parameters/OrderId" }
      ],
      "post": {
        "operationId": "acceptOrder",
        "tags": ["Orders"],
        "summary": "Accept an order",
        "description": "Confirms an incoming order (moves it `pending` → `confirmed`) and optionally sets the estimated preparation time shown to the customer. Call this promptly after receiving `order.created` — or set `auto_accept_orders` on the integration to skip the manual step.\n\nIf your app has more than one active store integration, pass `store_id` to disambiguate; an ambiguous request is rejected rather than routed to an arbitrary store.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AcceptOrderRequest"
              },
              "example": {
                "estimated_prep_time_minutes": 25
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderActionResponse"
                },
                "example": {
                  "status": "success",
                  "order_id": "ord_7d4e2k91mqx0",
                  "order_status": "confirmed",
                  "estimated_prep_time_minutes": 25
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "The order is not in a state that can be accepted (e.g. already cancelled by the customer).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "order.invalid_transition",
                    "message": "Order ord_7d4e2k91mqx0 is cancelled and cannot be accepted."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/orders/{order_id}/status": {
      "parameters": [
        { "$ref": "#/components/parameters/OrderId" }
      ],
      "post": {
        "operationId": "updateOrderStatus",
        "tags": ["Orders"],
        "summary": "Update order status",
        "description": "Reports the order's current state so the platform and customer stay in sync. Valid statuses: `pending`, `confirmed`, `preparing`, `ready`, `out_for_delivery`, `delivered`, `cancelled`, `failed`.\n\nWhen moving to `cancelled` or `failed`, include a structured `reason_code` so the platform can remediate automatically (e.g. `ITEM_OUT_OF_STOCK` triggers an availability resync prompt):\n\n| `reason_code` | Meaning |\n|---|---|\n| `ITEM_OUT_OF_STOCK` | One or more lines cannot be fulfilled — pair with an availability update |\n| `STORE_CLOSED` | The store is closed and cannot fulfil |\n| `POS_OFFLINE` | The POS terminal is offline / unreachable |\n| `CAPACITY` | Kitchen at capacity, throttling orders |\n| `PRICE_MISMATCH` | POS prices do not match the order — re-push the menu |\n| `OTHER` | Anything else — explain in `message` |",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateOrderStatusRequest"
              },
              "example": {
                "status": "preparing",
                "message": "Order is being prepared"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Status updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderActionResponse"
                },
                "example": {
                  "status": "success",
                  "order_id": "ord_7d4e2k91mqx0",
                  "order_status": "preparing"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "Invalid status transition for the order's current state.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "order.invalid_transition",
                    "message": "Cannot move order ord_7d4e2k91mqx0 from delivered to preparing."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/orders/{order_id}/ready": {
      "parameters": [
        { "$ref": "#/components/parameters/OrderId" }
      ],
      "post": {
        "operationId": "markOrderReady",
        "tags": ["Orders"],
        "summary": "Mark order ready",
        "description": "Shortcut that sets the order status to `ready` (for pickup, or for courier handoff on delivery orders). Equivalent to `POST /pos/orders/{order_id}/status` with `{ \"status\": \"ready\" }`. The body is optional — pass `store_id` only to disambiguate multi-store apps.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReadyOrderRequest"
              },
              "example": {
                "store_id": "st_9x2ml7qa41ce"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order marked ready.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderActionResponse"
                },
                "example": {
                  "status": "success",
                  "order_id": "ord_7d4e2k91mqx0",
                  "order_status": "ready"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/orders/{order_id}/cancel": {
      "parameters": [
        { "$ref": "#/components/parameters/OrderId" }
      ],
      "post": {
        "operationId": "cancelOrder",
        "tags": ["Orders"],
        "summary": "Cancel / deny an order",
        "description": "Cancels an order from the POS side — either denying a new order or cancelling an accepted one. A free-text `reason` is required (shown to the customer); pair it with a structured `reason_code` (`ITEM_OUT_OF_STOCK`, `STORE_CLOSED`, `POS_OFFLINE`, `CAPACITY`, `PRICE_MISMATCH`, `OTHER`) so the platform can react automatically. An `order.cancelled` webhook is emitted to all subscribed channels.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelOrderRequest"
              },
              "example": {
                "reason": "Out of stock",
                "reason_code": "ITEM_OUT_OF_STOCK",
                "message": "Sorry, we just ran out of fresh mozzarella."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelOrderResponse"
                },
                "example": {
                  "status": "success",
                  "order_id": "ord_7d4e2k91mqx0",
                  "order_status": "cancelled",
                  "reason": "Out of stock",
                  "reason_code": "ITEM_OUT_OF_STOCK"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "The order has already reached a terminal state.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "order.already_terminal",
                    "message": "Order ord_7d4e2k91mqx0 is already delivered and cannot be cancelled."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/webhooks": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" }
      ],
      "get": {
        "operationId": "getStoreWebhook",
        "tags": ["Webhooks"],
        "summary": "Get webhook configuration",
        "description": "Returns the store integration's webhook configuration. The secret is masked — the full `webhook_secret` is only ever returned by `POST /pos/integrate-store/{store_id}`.",
        "responses": {
          "200": {
            "description": "Current webhook configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoreWebhookConfig"
                },
                "example": {
                  "store_id": "st_9x2ml7qa41ce",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy",
                  "webhook_secret_masked": "whsec_4f...",
                  "status": "active"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "post": {
        "operationId": "setStoreWebhook",
        "tags": ["Webhooks"],
        "summary": "Set webhook URL",
        "description": "Sets (or replaces) the webhook URL for this store's integration. The URL must be a public HTTPS endpoint — private, loopback, link-local, and cloud-metadata targets are rejected. Redirects are treated as delivery failures, so point directly at your handler.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetWebhookRequest"
              },
              "example": {
                "url": "https://pos.example.com/webhooks/quiqqy"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook URL set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWriteResponse"
                },
                "example": {
                  "status": "success",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": {
            "description": "The URL failed validation (not public HTTPS, or points at a private / metadata address).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "webhook.url_not_allowed",
                    "message": "Webhook URLs must resolve to a public HTTPS endpoint."
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/pos/stores/{store_id}/webhooks/{webhook_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/StoreId" },
        { "$ref": "#/components/parameters/WebhookId" }
      ],
      "put": {
        "operationId": "updateStoreWebhook",
        "tags": ["Webhooks"],
        "summary": "Update webhook URL",
        "description": "Updates the webhook URL on an existing integration webhook. Same URL validation rules as `POST /pos/stores/{store_id}/webhooks`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookRequest"
              },
              "example": {
                "url": "https://pos.example.com/webhooks/quiqqy/v2"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookWriteResponse"
                },
                "example": {
                  "status": "success",
                  "webhook_url": "https://pos.example.com/webhooks/quiqqy/v2"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "delete": {
        "operationId": "deleteStoreWebhook",
        "tags": ["Webhooks"],
        "summary": "Remove webhook URL",
        "description": "Removes the webhook URL from the integration. Event deliveries stop until a new URL is set. The integration itself stays active.",
        "responses": {
          "200": {
            "description": "Webhook URL removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SimpleStatusResponse"
                },
                "example": {
                  "status": "success",
                  "message": "Webhook URL removed"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    }
  },
  "webhooks": {
    "order.created": {
      "post": {
        "operationId": "webhookOrderCreated",
        "tags": ["Webhooks"],
        "summary": "order.created — a new order was placed",
        "description": "Sent when a customer places a new order at an integrated store. `data` is the **full order** — items with modifiers and your `external_id`s, totals in minor units, customer, and `fulfillment_type`.\n\nRespond `2xx` fast, then accept via `POST /pos/orders/{order_id}/accept` (or rely on `auto_accept_orders`). Deny with `POST /pos/orders/{order_id}/cancel` + a structured `reason_code`.\n\n**Signing & delivery (all events):** every delivery carries `X-Quiqqy-Signature` — HMAC-SHA256 of the **raw request body** using your integration's `webhook_secret`, lowercase hex. Verify with a constant-time compare before trusting the payload. Deliveries time out after 10 s; 3xx responses count as failures. Failed deliveries are retried on a `1m, 5m, 30m, 2h, 6h` ladder — **6 attempts total**. Deliveries can repeat: dedupe on `X-Quiqqy-Delivery-Id` / `event_id`.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderEventEnvelope"
              },
              "example": {
                "event_type": "order.created",
                "event_id": "evt_7a1e9c2d4f8b",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T12:34:56.000Z",
                "data": {
                  "id": "ord_7d4e2k91mqx0",
                  "order_number": "ORD-20260730-0001",
                  "fulfillment_type": "delivery",
                  "order_type": "delivery",
                  "status": "pending",
                  "payment_status": "paid",
                  "currency": "USD",
                  "subtotal": 1849,
                  "tax": 164,
                  "delivery_fee": 200,
                  "discount": 0,
                  "tip": 150,
                  "total_amount": 2363,
                  "prices_include_tax": false,
                  "special_instructions": "Ring the doorbell twice",
                  "customer_id": "cus_c1a2d94k3mx7",
                  "customer": {
                    "id": "cus_c1a2d94k3mx7",
                    "name": "Anita R.",
                    "phone": "+14155550187",
                    "email": "anita@example.com"
                  },
                  "delivery_address": {
                    "street_address": "88 Prospect Ave, Apt 4B",
                    "city": "New York",
                    "state": "NY",
                    "country": "US",
                    "zip": "11215"
                  },
                  "created_at": "2026-07-30T12:34:56.000Z",
                  "items": [
                    {
                      "product_id": "prd_aa11e0b2c9d4",
                      "product_variant_id": "var_bb22f1c3d0e5",
                      "product_name": "Margherita Pizza (Large)",
                      "quantity": 1,
                      "unit_price": 1699,
                      "subtotal": 1699,
                      "tax_amount": 151,
                      "external_id": "SKU-MARG",
                      "external_source": "app-app_2c9d7fk1qz84",
                      "variant_external_id": "SKU-MARG-LG",
                      "variant_external_source": "app-app_2c9d7fk1qz84",
                      "options": [
                        {
                          "name": "Extra cheese",
                          "price": 150,
                          "quantity": 1,
                          "external_id": "addon-xcheese"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery. Non-2xx (including 3xx) triggers the retry ladder."
          }
        }
      }
    },
    "order.updated": {
      "post": {
        "operationId": "webhookOrderUpdated",
        "tags": ["Webhooks"],
        "summary": "order.updated — order status advanced",
        "description": "Sent when an order's status changes (e.g. `confirmed` → `preparing` → `ready` → `out_for_delivery`). `data` is the full order with the new `status` — same shape as `order.created`. Same signing headers and retry ladder as all events.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderEventEnvelope"
              },
              "example": {
                "event_type": "order.updated",
                "event_id": "evt_8b2f0d3e5a9c",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T12:41:00.000Z",
                "data": {
                  "id": "ord_7d4e2k91mqx0",
                  "order_number": "ORD-20260730-0001",
                  "fulfillment_type": "delivery",
                  "status": "preparing",
                  "payment_status": "paid",
                  "currency": "USD",
                  "subtotal": 1849,
                  "tax": 164,
                  "delivery_fee": 200,
                  "discount": 0,
                  "tip": 150,
                  "total_amount": 2363,
                  "created_at": "2026-07-30T12:34:56.000Z",
                  "items": []
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "order.cancelled": {
      "post": {
        "operationId": "webhookOrderCancelled",
        "tags": ["Webhooks"],
        "summary": "order.cancelled — order was cancelled",
        "description": "Sent when an order is cancelled — by the customer, the platform, or your own `POST /pos/orders/{order_id}/cancel`. `data` is the full order with `status: \"cancelled\"` plus `cancellation_reason` (and `cancellation_reason_code` when a structured code was supplied). Same signing headers and retry ladder as all events.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderEventEnvelope"
              },
              "example": {
                "event_type": "order.cancelled",
                "event_id": "evt_9c3a1e4f6b0d",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T12:45:00.000Z",
                "data": {
                  "id": "ord_7d4e2k91mqx0",
                  "order_number": "ORD-20260730-0001",
                  "fulfillment_type": "delivery",
                  "status": "cancelled",
                  "payment_status": "refunded",
                  "cancellation_reason": "Out of stock",
                  "cancellation_reason_code": "ITEM_OUT_OF_STOCK",
                  "currency": "USD",
                  "subtotal": 1849,
                  "tax": 164,
                  "delivery_fee": 200,
                  "discount": 0,
                  "tip": 150,
                  "total_amount": 2363,
                  "created_at": "2026-07-30T12:34:56.000Z",
                  "items": []
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "order.delivered": {
      "post": {
        "operationId": "webhookOrderDelivered",
        "tags": ["Webhooks"],
        "summary": "order.delivered — order completed",
        "description": "Sent when an order reaches its terminal fulfilled state (`delivered` for delivery orders; emitted on completion for pickup and dine-in). `data` is the full order with `status: \"delivered\"`. Same signing headers and retry ladder as all events.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OrderEventEnvelope"
              },
              "example": {
                "event_type": "order.delivered",
                "event_id": "evt_a1d40f5e7c2b",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T13:10:00.000Z",
                "data": {
                  "id": "ord_7d4e2k91mqx0",
                  "order_number": "ORD-20260730-0001",
                  "fulfillment_type": "delivery",
                  "status": "delivered",
                  "payment_status": "paid",
                  "currency": "USD",
                  "subtotal": 1849,
                  "tax": 164,
                  "delivery_fee": 200,
                  "discount": 0,
                  "tip": 150,
                  "total_amount": 2363,
                  "created_at": "2026-07-30T12:34:56.000Z",
                  "items": []
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "menu.updated": {
      "post": {
        "operationId": "webhookMenuUpdated",
        "tags": ["Webhooks"],
        "summary": "menu.updated — catalogue changed in Quiqqy",
        "description": "Sent when the merchant edits the catalogue inside Quiqqy (relevant when your integration's `menu_sync_direction` is `quiqqy_to_pos` or `bidirectional`). Treat it as a signal to pull or reconcile — the payload summarises what changed rather than carrying the full menu. Same signing headers and retry ladder as all events.\n\n> Build your handler to accept-and-ignore unknown events: this event is part of the contract but may not yet be emitted for every catalogue change.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MenuUpdatedEnvelope"
              },
              "example": {
                "event_type": "menu.updated",
                "event_id": "evt_b3c52a6d8e1f",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T08:02:11.000Z",
                "data": {
                  "menu_external_id": "menu-main",
                  "changed": ["items", "categories"],
                  "source": "merchant_portal"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "menu.item_availability_changed": {
      "post": {
        "operationId": "webhookItemAvailabilityChanged",
        "tags": ["Webhooks"],
        "summary": "menu.item_availability_changed — items 86'd or restored in Quiqqy",
        "description": "Sent when item availability changes on the Quiqqy side (merchant portal or another channel), so your POS can mirror the 86. `data.items[]` carries your `external_id` where the item was pushed by your app. Same signing headers and retry ladder as all events.\n\n> Build your handler to accept-and-ignore unknown events: this event is part of the contract but may not yet be emitted for every availability change.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ItemAvailabilityEnvelope"
              },
              "example": {
                "event_type": "menu.item_availability_changed",
                "event_id": "evt_c4d63b7e9f20",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T11:20:45.000Z",
                "data": {
                  "items": [
                    {
                      "product_id": "prd_aa11e0b2c9d4",
                      "external_id": "SKU-MARG",
                      "available": false
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "store.status_changed": {
      "post": {
        "operationId": "webhookStoreStatusChanged",
        "tags": ["Webhooks"],
        "summary": "store.status_changed — store went live or paused",
        "description": "Sent when the store's online-ordering status changes (paused from the merchant portal, reactivated, etc.), so your POS can reflect it. Same signing headers and retry ladder as all events.\n\n> Build your handler to accept-and-ignore unknown events: this event is part of the contract but may not yet be emitted for every status change.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StoreStatusEnvelope"
              },
              "example": {
                "event_type": "store.status_changed",
                "event_id": "evt_d5e74c8f0a31",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T14:00:00.000Z",
                "data": {
                  "status": "inactive",
                  "previous_status": "active",
                  "changed_by": "merchant_portal"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    },
    "action_required": {
      "post": {
        "operationId": "webhookActionRequired",
        "tags": ["Webhooks"],
        "summary": "action_required — Quiqqy asks your POS to act",
        "description": "The command channel from Quiqqy to your POS. The requested command is in **`data.action`** (also mirrored in the `X-Quiqqy-Action` header, and as the legacy alias `data.request_type`), so you dispatch on the single `action_required` event:\n\n| `data.action` | What your POS should do |\n|---|---|\n| `menu_push` | Push your latest full menu via `POST /pos/stores/{store_id}/menus` |\n| `resync` | Re-sync your whole catalogue (treat as a full menu push) |\n\nAcknowledge with `2xx` first, then perform the action asynchronously. Same signing headers and retry ladder as all events.",
        "parameters": [
          { "$ref": "#/components/parameters/XQuiqqySignature" },
          { "$ref": "#/components/parameters/XQuiqqyEvent" },
          { "$ref": "#/components/parameters/XQuiqqyDeliveryId" },
          { "$ref": "#/components/parameters/XQuiqqyLocationId" },
          { "$ref": "#/components/parameters/XQuiqqyAction" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActionRequiredEnvelope"
              },
              "example": {
                "event_type": "action_required",
                "event_id": "evt_b2e51d6c8a4f",
                "store_id": "st_9x2ml7qa41ce",
                "location_id": "LITHOS-LOC-001",
                "timestamp": "2026-07-30T09:00:00.000Z",
                "data": {
                  "action": "menu_push",
                  "request_type": "menu_push",
                  "app_id": "app_2c9d7fk1qz84",
                  "integration_id": "int_5rk20dq8xw3f",
                  "requested_by": "developer_portal"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge, then perform the requested action."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "**Merchant access token** (OAuth 2.0 authorization-code flow). Obtain it with your app's client credentials:\n\n1. `POST /pos/signup_url` (Basic auth) → redirect the merchant to the returned `signup_url`.\n2. Merchant authorizes; you receive `?code=...` on your `redirect_uri`.\n3. `POST /oauth2/token` with `{ code, redirect_uri, client_id }` → `{ access_token, refresh_token, expires_in }`.\n4. Send `Authorization: Bearer <access_token>` on every `/pos/*` call; refresh with `POST /oauth2/refresh`.\n\nThe token is minted from your `client_id` and carries your app identity — do not send `app_id` in request bodies. API keys are rejected on `/pos/*` routes."
      },
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "**Client credentials** for the onboarding bootstrap only (`POST /pos/signup_url`): username = `client_id`, password = `client_secret`, both issued in your app's Credentials tab in the developer portal. Keep the secret server-side — never ship it in a POS terminal build."
      }
    },
    "parameters": {
      "StoreId": {
        "name": "store_id",
        "in": "path",
        "required": true,
        "description": "Quiqqy store id, from `GET /pos/stores`.",
        "schema": {
          "type": "string",
          "examples": ["st_9x2ml7qa41ce"]
        }
      },
      "OrderId": {
        "name": "order_id",
        "in": "path",
        "required": true,
        "description": "Quiqqy order id — `data.id` from the `order.created` webhook.",
        "schema": {
          "type": "string",
          "examples": ["ord_7d4e2k91mqx0"]
        }
      },
      "JobId": {
        "name": "job_id",
        "in": "path",
        "required": true,
        "description": "Menu sync job id, from the 202 response of a menu push.",
        "schema": {
          "type": "string",
          "examples": ["job_8m2xk40qc7ra"]
        }
      },
      "ExtItemId": {
        "name": "ext_item_id",
        "in": "path",
        "required": true,
        "description": "**Your** external item id (the `external_id` you pushed).",
        "schema": {
          "type": "string",
          "examples": ["SKU-MARG"]
        }
      },
      "WebhookId": {
        "name": "webhook_id",
        "in": "path",
        "required": true,
        "description": "Webhook (integration) id.",
        "schema": {
          "type": "string",
          "examples": ["int_5rk20dq8xw3f"]
        }
      },
      "XQuiqqySignature": {
        "name": "X-Quiqqy-Signature",
        "in": "header",
        "required": true,
        "description": "HMAC-SHA256 of the raw request body using your `webhook_secret`, lowercase hex. Verify with a constant-time compare against the raw bytes — not re-serialized JSON.",
        "schema": {
          "type": "string",
          "examples": ["a3f1c2e94b7d8065f4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0"]
        }
      },
      "XQuiqqyEvent": {
        "name": "X-Quiqqy-Event",
        "in": "header",
        "required": true,
        "description": "The event type, mirroring `event_type` in the body.",
        "schema": {
          "type": "string",
          "examples": ["order.created"]
        }
      },
      "XQuiqqyDeliveryId": {
        "name": "X-Quiqqy-Delivery-Id",
        "in": "header",
        "required": true,
        "description": "Unique delivery id (equals `event_id`). Deliveries can repeat — use this for idempotency.",
        "schema": {
          "type": "string",
          "examples": ["evt_7a1e9c2d4f8b"]
        }
      },
      "XQuiqqyLocationId": {
        "name": "X-Quiqqy-Location-Id",
        "in": "header",
        "required": false,
        "description": "Your `external_store_id`, when set on the integration. Route the event to the right POS location on this header.",
        "schema": {
          "type": "string",
          "examples": ["LITHOS-LOC-001"]
        }
      },
      "XQuiqqyAction": {
        "name": "X-Quiqqy-Action",
        "in": "header",
        "required": false,
        "description": "Only on `action_required` events — equals `data.action`.",
        "schema": {
          "type": "string",
          "enum": ["menu_push", "resync"]
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or invalid credentials. Refresh the access token and retry.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "auth.invalid_token",
                "message": "The access token is missing, expired, or invalid."
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "The token is valid but not allowed to perform this action — the app install was revoked or suspended, or the operation is not permitted for this integration.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "auth.app_install_revoked",
                "message": "The merchant has uninstalled this app. Stop syncing and prompt the merchant to reinstall."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The store, order, or resource does not exist, or there is no active integration for your app.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "integration.not_found",
                "message": "No active integration found for this store."
              }
            }
          }
        }
      },
      "Conflict": {
        "description": "The request conflicts with the resource's current state.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "resource.conflict",
                "message": "The request conflicts with the current state of the resource."
              }
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "The body is well-formed JSON but failed validation. Note that unknown fields (including `app_id`) are rejected.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "validation.failed",
                "message": "items[0].price must be an integer in minor units."
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded for your app. Honour `Retry-After` and back off.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "rate_limit.exceeded",
                "message": "Too many requests. Retry after the indicated delay."
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every 4xx/5xx response uses this shape. `code` is a stable, dot-namespaced machine code — branch on it, not on `message`.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable dot-namespaced error code, e.g. `auth.invalid_token`, `integration.not_found`, `order.invalid_transition`, `menu.push_not_allowed`, `validation.failed`, `rate_limit.exceeded`.",
                "examples": ["order.not_found"]
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation. May change between releases — do not parse."
              }
            }
          }
        }
      },
      "Address": {
        "type": "object",
        "required": ["street_address", "city", "country"],
        "properties": {
          "street_address": { "type": "string" },
          "city": { "type": "string" },
          "state": { "type": ["string", "null"] },
          "country": { "type": "string", "description": "ISO 3166-1 alpha-2 country code." },
          "zip": { "type": ["string", "null"] }
        }
      },
      "SignupUrlRequest": {
        "type": "object",
        "required": ["location_id", "store_name", "email"],
        "properties": {
          "location_id": {
            "type": "string",
            "description": "Your POS location/store identifier. Echoed back on webhooks as `X-Quiqqy-Location-Id` once set as `external_store_id`.",
            "examples": ["LITHOS-LOC-001"]
          },
          "store_name": { "type": "string", "description": "Display name of the store." },
          "email": { "type": "string", "format": "email", "description": "Merchant email — becomes the merchant account login." },
          "business_phone": { "type": "string" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "address": { "$ref": "#/components/schemas/Address" },
          "locale": { "type": "string", "examples": ["en-US"] },
          "redirect_uri": {
            "type": "string",
            "format": "uri",
            "description": "Where the merchant is sent after authorizing. Must match one of your app's Allowed Redirect URIs."
          },
          "state": { "type": "string", "description": "Opaque CSRF value, echoed back on the redirect." },
          "flow_type": {
            "type": "string",
            "enum": ["login", "signup"],
            "description": "`login` = existing merchant; `signup` (default) = create a new merchant."
          },
          "client_id": {
            "type": "string",
            "description": "Your OAuth client id. Optional when sent as the Basic-auth username."
          }
        }
      },
      "SignupUrlResponse": {
        "type": "object",
        "required": ["status", "signup_url"],
        "properties": {
          "status": { "type": "string", "const": "success" },
          "signup_url": {
            "type": "string",
            "format": "uri",
            "description": "OAuth authorize URL. Redirect the merchant here."
          }
        }
      },
      "StoreStatus": {
        "type": "string",
        "enum": ["active", "inactive"],
        "description": "`active` = accepting online orders; `inactive` = paused."
      },
      "Store": {
        "type": "object",
        "required": ["store_id", "name", "status", "integration_status"],
        "properties": {
          "store_id": { "type": "string", "examples": ["st_9x2ml7qa41ce"] },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "status": { "$ref": "#/components/schemas/StoreStatus" },
          "integration_status": {
            "type": "string",
            "enum": ["not_integrated", "integrated", "disabled", "pending"],
            "description": "Integration state between this store and **your** app."
          },
          "integration": {
            "oneOf": [
              { "$ref": "#/components/schemas/Integration" },
              { "type": "null" }
            ],
            "description": "Present when integrated. Never includes `webhook_secret`."
          }
        }
      },
      "MenuSyncDirection": {
        "type": "string",
        "enum": ["none", "quiqqy_to_pos", "pos_to_quiqqy", "bidirectional"],
        "description": "Who owns the catalogue: `pos_to_quiqqy` = the POS pushes menus (typical); `quiqqy_to_pos` = the merchant edits in Quiqqy and the POS pulls; `bidirectional` = both."
      },
      "Integration": {
        "type": "object",
        "required": ["integration_id", "store_id", "menu_sync_direction", "status"],
        "properties": {
          "integration_id": { "type": "string", "examples": ["int_5rk20dq8xw3f"] },
          "app_id": { "type": "string", "examples": ["app_2c9d7fk1qz84"] },
          "store_id": { "type": "string", "examples": ["st_9x2ml7qa41ce"] },
          "webhook_url": { "type": ["string", "null"], "format": "uri" },
          "webhook_secret": {
            "type": "string",
            "description": "Only returned by `POST /pos/integrate-store/{store_id}`. Used to verify `X-Quiqqy-Signature` on every webhook. Store server-side.",
            "examples": ["whsec_4f7a1e0b9c2d8e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f"]
          },
          "menu_sync_direction": { "$ref": "#/components/schemas/MenuSyncDirection" },
          "auto_accept_orders": {
            "type": "boolean",
            "description": "When true, incoming orders are confirmed automatically and you receive them already `confirmed`."
          },
          "external_store_id": {
            "type": ["string", "null"],
            "description": "Your POS location id. Sent on every webhook as `X-Quiqqy-Location-Id`."
          },
          "notification_email": { "type": ["string", "null"], "format": "email" },
          "status": {
            "type": "string",
            "enum": ["active", "inactive", "pending"]
          },
          "connected_at": { "type": "string", "format": "date-time" }
        }
      },
      "IntegrateStoreRequest": {
        "type": "object",
        "properties": {
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTPS endpoint to receive order/menu events."
          },
          "notification_email": { "type": "string", "format": "email" },
          "menu_sync_direction": { "$ref": "#/components/schemas/MenuSyncDirection" },
          "external_store_id": {
            "type": "string",
            "description": "Your POS location id for this store."
          },
          "auto_accept_orders": { "type": "boolean", "default": false }
        }
      },
      "UpdateIntegrationRequest": {
        "type": "object",
        "description": "All fields optional; only sent fields are changed.",
        "properties": {
          "webhook_url": { "type": "string", "format": "uri" },
          "notification_email": { "type": "string", "format": "email" },
          "menu_sync_direction": { "$ref": "#/components/schemas/MenuSyncDirection" },
          "auto_accept_orders": { "type": "boolean" }
        }
      },
      "StoreSettingsRequest": {
        "type": "object",
        "description": "Operational store settings. **Field names are camelCase** — they mirror the merchant store-settings object and are forwarded verbatim.",
        "properties": {
          "name": { "type": "string" },
          "description": { "type": "string" },
          "imageUrl": { "type": "string", "format": "uri" },
          "subdomain": { "type": "string", "description": "Storefront subdomain under quiqqy.app." },
          "status": { "$ref": "#/components/schemas/StoreStatus" },
          "address": { "type": "object", "additionalProperties": true },
          "availability": {
            "type": "object",
            "additionalProperties": true,
            "description": "Opening hours by weekday, e.g. `{ \"monday\": [{ \"open\": \"11:00\", \"close\": \"22:00\" }] }`."
          },
          "deliverySettings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Delivery configuration — `enabled`, `min_order_amount` (minor units), `delivery_fee` (minor units), `radius_km`."
          },
          "notificationSettings": { "type": "object", "additionalProperties": true },
          "contactDetails": { "type": "object", "additionalProperties": true },
          "features": { "type": "object", "additionalProperties": true }
        }
      },
      "SetStoreStatusRequest": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "$ref": "#/components/schemas/StoreStatus" }
        }
      },
      "TaxConfig": {
        "type": "object",
        "required": ["enabled", "tax_level", "pricing_mode", "default_tax_id", "taxes"],
        "properties": {
          "enabled": { "type": "boolean" },
          "tax_level": {
            "type": "string",
            "enum": ["store", "category", "product"],
            "description": "Where tax applies. `product` means your menu push should declare per-item `tax`."
          },
          "pricing_mode": {
            "type": "string",
            "enum": ["inclusive", "exclusive"],
            "description": "Whether displayed prices include tax."
          },
          "default_tax_id": { "type": ["string", "null"] },
          "taxes": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["tax_id", "name", "components"],
              "properties": {
                "tax_id": { "type": "string" },
                "name": { "type": "string" },
                "components": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/TaxComponent" }
                }
              }
            }
          }
        }
      },
      "TaxComponent": {
        "type": "object",
        "required": ["agency", "rate"],
        "properties": {
          "agency": { "type": "string", "description": "Taxing agency name." },
          "rate": { "type": "number", "description": "Percentage rate, e.g. `4.875`." },
          "compound": { "type": "boolean", "default": false }
        }
      },
      "MenuTax": {
        "type": "object",
        "description": "Self-describing per-item/category tax declaration. Quiqqy resolves it to a tax definition (find-or-create by `name`).",
        "required": ["name"],
        "properties": {
          "name": { "type": "string", "examples": ["NYC Sales Tax"] },
          "code": { "type": "string" },
          "rate": { "type": "number", "description": "Flat percentage rate — alternative to `components`." },
          "components": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/TaxComponent" }
          }
        }
      },
      "VariantCombination": {
        "type": "object",
        "required": ["attribute", "value_key", "value_label"],
        "properties": {
          "attribute": { "type": "string", "description": "Attribute name (find-or-create), e.g. `Size`.", "examples": ["Size"] },
          "value_key": { "type": "string", "examples": ["lg"] },
          "value_label": { "type": "string", "examples": ["Large"] },
          "price_difference": { "type": "integer", "description": "Minor units delta against the base price." },
          "image_url": { "type": "string", "format": "uri" }
        }
      },
      "MenuItemVariant": {
        "type": "object",
        "required": ["external_id", "price"],
        "properties": {
          "external_id": {
            "type": "string",
            "description": "Your stable variant/SKU id (idempotency key). Returned on order lines as `variant_external_id`.",
            "examples": ["SKU-MARG-LG"]
          },
          "variant_name": { "type": "string", "examples": ["Large"] },
          "variant_key": { "type": "string" },
          "price": { "type": "integer", "description": "Minor units.", "examples": [1699] },
          "stock": { "type": "integer", "minimum": 0 },
          "image_url": { "type": "string", "format": "uri" },
          "available": { "type": "boolean" },
          "consumption_ratio": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "description": "Only with `variant_type: consumption_ratio` — how much of the parent stock one unit consumes."
          },
          "combinations": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/VariantCombination" }
          }
        }
      },
      "MenuItem": {
        "type": "object",
        "required": ["external_id", "name", "price"],
        "properties": {
          "external_id": {
            "type": "string",
            "description": "**Your** stable POS item id (idempotency key). Echoed back on order lines as `items[].external_id`.",
            "examples": ["SKU-MARG"]
          },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "price": { "type": "integer", "description": "Minor units of the store currency.", "examples": [1299] },
          "categories": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Category `external_id`s (or names) this item belongs to."
          },
          "addon_groups": {
            "type": "array",
            "items": { "type": "string" },
            "description": "`addon_group` external_ids attached to this item."
          },
          "image_url": { "type": "string", "format": "uri" },
          "available": { "type": "boolean", "default": true },
          "product_type": {
            "type": "string",
            "enum": ["product", "addon"],
            "default": "product",
            "description": "`addon` = non-sellable modifier: hidden from the storefront listing and rejected as a standalone order line."
          },
          "tax_exempt": { "type": "boolean" },
          "tax": { "$ref": "#/components/schemas/MenuTax" },
          "variant_type": {
            "type": "string",
            "enum": ["none", "stand_alone", "consumption_ratio"],
            "description": "`stand_alone` = each variant has its own price and stock; `consumption_ratio` = variants share parent stock at a 1–100 ratio."
          },
          "variants": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuItemVariant" },
            "description": "A push replaces the item's variant set exactly — a dropped variant is deactivated."
          }
        }
      },
      "AddOn": {
        "type": "object",
        "required": ["external_id", "name", "price"],
        "properties": {
          "external_id": { "type": "string", "examples": ["addon-xcheese"] },
          "name": { "type": "string", "examples": ["Extra cheese"] },
          "price": { "type": "integer", "description": "Minor units.", "examples": [150] }
        }
      },
      "AddonGroup": {
        "type": "object",
        "required": ["external_id", "name", "add_ons"],
        "properties": {
          "external_id": { "type": "string", "examples": ["grp-toppings"] },
          "name": { "type": "string", "examples": ["Toppings"] },
          "description": { "type": "string" },
          "min_select": { "type": "integer", "minimum": 0 },
          "max_select": { "type": "integer", "minimum": 1 },
          "required": { "type": "boolean", "default": false },
          "add_ons": {
            "type": "array",
            "items": { "type": "string" },
            "description": "`external_id`s of the add-ons in this group."
          }
        }
      },
      "MenuCategory": {
        "type": "object",
        "required": ["name"],
        "properties": {
          "external_id": { "type": "string", "examples": ["cat-pizza"] },
          "name": { "type": "string", "examples": ["Pizza"] },
          "description": { "type": "string" },
          "image_url": { "type": "string", "format": "uri" },
          "tax_exempt": { "type": "boolean" },
          "tax": { "$ref": "#/components/schemas/MenuTax" }
        }
      },
      "MenuBundle": {
        "type": "object",
        "required": ["name", "items"],
        "properties": {
          "external_id": { "type": "string", "examples": ["menu-main"] },
          "name": { "type": "string", "examples": ["Main Menu"] },
          "description": { "type": "string" },
          "availability": {
            "type": "object",
            "additionalProperties": true,
            "description": "Menu-level serving hours, e.g. `{ \"monday\": [{ \"open\": \"11:00\", \"close\": \"15:00\" }] }`."
          },
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuCategory" }
          },
          "add_ons": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddOn" }
          },
          "addon_groups": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddonGroup" }
          },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuItem" },
            "description": "Directly-sellable products only — modifiers go in `add_ons` (or use `product_type: addon`)."
          }
        }
      },
      "MenuPushRequest": {
        "type": "object",
        "description": "Either the preferred `menus[]` form or the flat single-menu form (top-level `menu_name` + `items`).",
        "properties": {
          "menus": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuBundle" },
            "description": "Preferred: one or more named menus, each with its own categories/items."
          },
          "menu_name": { "type": "string", "description": "Flat form: menu display name." },
          "menu_external_id": { "type": "string", "description": "Flat form: your menu id." },
          "menu_description": { "type": "string" },
          "availability": { "type": "object", "additionalProperties": true },
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuCategory" }
          },
          "add_ons": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddOn" }
          },
          "addon_groups": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddonGroup" }
          },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuItem" }
          }
        }
      },
      "MenuPushSyncResponse": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["success", "partial"],
            "description": "`partial` = accepted, but downstream forwarding reported an issue (see `error`)."
          },
          "message": { "type": "string" },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Result counts from the catalogue service."
          },
          "error": { "type": "string", "description": "Downstream error detail when `status` is `partial`." }
        }
      },
      "MenuPushAsyncResponse": {
        "type": "object",
        "required": ["status", "job_id", "poll_url"],
        "properties": {
          "status": { "type": "string", "const": "accepted" },
          "job_id": { "type": "string", "examples": ["job_8m2xk40qc7ra"] },
          "message": { "type": "string" },
          "poll_url": {
            "type": "string",
            "description": "Base-relative URL to poll: `GET /pos/stores/{store_id}/menus/jobs/{job_id}`."
          }
        }
      },
      "MenuSyncJob": {
        "type": "object",
        "required": ["job_id", "store_id", "status", "menus", "items", "created_at", "updated_at"],
        "properties": {
          "job_id": { "type": "string" },
          "store_id": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["processing", "success", "failed"]
          },
          "menus": { "type": "integer", "description": "Number of menus in the push." },
          "items": { "type": "integer", "description": "Number of items in the push." },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "result": {
            "type": "object",
            "additionalProperties": true,
            "description": "Catalogue result counts, on success."
          },
          "error": { "type": "string", "description": "Error message, on failure." }
        }
      },
      "MenuSyncStatus": {
        "type": "object",
        "required": ["store_id", "menu_sync_direction", "integration_status"],
        "properties": {
          "store_id": { "type": "string" },
          "menu_sync_direction": { "$ref": "#/components/schemas/MenuSyncDirection" },
          "integration_status": {
            "type": "string",
            "enum": ["active", "inactive", "pending"]
          },
          "last_synced_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "BatchMenuOperationRequest": {
        "type": "object",
        "properties": {
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuCategory" },
            "description": "Category definitions referenced by added/updated items."
          },
          "add_ons": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddOn" }
          },
          "addon_groups": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/AddonGroup" }
          },
          "add": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuItem" }
          },
          "update": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/MenuItem" }
          },
          "delete": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Item `external_id`s to remove."
          }
        }
      },
      "BatchOperationResponse": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": { "type": "string", "enum": ["success", "partial"] },
          "message": { "type": "string" },
          "added": { "type": "integer" },
          "updated": { "type": "integer" },
          "deleted": { "type": "integer" }
        }
      },
      "ItemAvailability": {
        "type": "object",
        "required": ["external_id", "available"],
        "properties": {
          "external_id": { "type": "string", "examples": ["SKU-MARG"] },
          "available": { "type": "boolean" }
        }
      },
      "BatchAvailabilityRequest": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/ItemAvailability" }
          }
        }
      },
      "AvailabilityResponse": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": { "type": "string", "enum": ["success", "partial"] },
          "message": { "type": "string" },
          "updated": { "type": "integer" },
          "not_found": {
            "type": "array",
            "items": { "type": "string" },
            "description": "`external_id`s that did not match any item pushed by your app."
          }
        }
      },
      "TablesRequest": {
        "type": "object",
        "required": ["tables"],
        "properties": {
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["external_id", "name"],
              "properties": {
                "external_id": { "type": "string", "examples": ["T-01"] },
                "name": { "type": "string", "examples": ["Table 1"] },
                "seats": { "type": "integer", "minimum": 1 },
                "zone": { "type": "string", "examples": ["Terrace"] }
              }
            }
          }
        }
      },
      "TablesResponse": {
        "type": "object",
        "required": ["status", "tables"],
        "properties": {
          "status": { "type": "string", "enum": ["success", "partial"] },
          "tables": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["external_id", "table_id", "qr_token"],
              "properties": {
                "external_id": { "type": "string" },
                "table_id": { "type": "string", "examples": ["tbl_3fq81mz2vk9c"] },
                "qr_token": { "type": "string", "examples": ["qrt_c81d2e9f4a5b"] },
                "qr_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Storefront ordering URL for this table — encode it into the printed QR code."
                }
              }
            }
          }
        }
      },
      "OrderStatus": {
        "type": "string",
        "enum": ["pending", "confirmed", "preparing", "ready", "out_for_delivery", "delivered", "cancelled", "failed"]
      },
      "CancelReasonCode": {
        "type": "string",
        "enum": ["ITEM_OUT_OF_STOCK", "STORE_CLOSED", "POS_OFFLINE", "CAPACITY", "PRICE_MISMATCH", "OTHER"],
        "description": "Structured cancellation / denial reason. Enables automated remediation — e.g. `ITEM_OUT_OF_STOCK` prompts an availability resync, `PRICE_MISMATCH` prompts a menu push."
      },
      "AcceptOrderRequest": {
        "type": "object",
        "properties": {
          "estimated_prep_time_minutes": {
            "type": "integer",
            "minimum": 0,
            "description": "Estimated minutes until the order is ready. Shown to the customer."
          },
          "store_id": {
            "type": "string",
            "description": "Only needed when your app has multiple active store integrations — disambiguates which store's integration handles the order."
          }
        }
      },
      "UpdateOrderStatusRequest": {
        "type": "object",
        "required": ["status"],
        "properties": {
          "status": { "$ref": "#/components/schemas/OrderStatus" },
          "message": { "type": "string", "description": "Optional customer-facing message." },
          "reason_code": {
            "$ref": "#/components/schemas/CancelReasonCode",
            "description": "Include when `status` is `cancelled` or `failed`."
          },
          "store_id": { "type": "string", "description": "Disambiguates multi-store apps." }
        }
      },
      "CancelOrderRequest": {
        "type": "object",
        "required": ["reason"],
        "properties": {
          "reason": { "type": "string", "description": "Customer-facing cancellation reason.", "examples": ["Out of stock"] },
          "reason_code": { "$ref": "#/components/schemas/CancelReasonCode" },
          "message": { "type": "string", "description": "Optional additional message." },
          "store_id": { "type": "string", "description": "Disambiguates multi-store apps." }
        }
      },
      "ReadyOrderRequest": {
        "type": "object",
        "properties": {
          "store_id": { "type": "string", "description": "Only needed when your app has multiple active store integrations." }
        }
      },
      "OrderActionResponse": {
        "type": "object",
        "required": ["status", "order_id", "order_status"],
        "properties": {
          "status": { "type": "string", "const": "success" },
          "order_id": { "type": "string", "examples": ["ord_7d4e2k91mqx0"] },
          "order_status": { "$ref": "#/components/schemas/OrderStatus" },
          "estimated_prep_time_minutes": { "type": "integer" }
        }
      },
      "CancelOrderResponse": {
        "type": "object",
        "required": ["status", "order_id", "order_status", "reason"],
        "properties": {
          "status": { "type": "string", "const": "success" },
          "order_id": { "type": "string" },
          "order_status": { "type": "string", "const": "cancelled" },
          "reason": { "type": "string" },
          "reason_code": { "$ref": "#/components/schemas/CancelReasonCode" }
        }
      },
      "StoreWebhookConfig": {
        "type": "object",
        "required": ["store_id", "webhook_url", "status"],
        "properties": {
          "store_id": { "type": "string" },
          "webhook_url": { "type": ["string", "null"], "format": "uri" },
          "webhook_secret_masked": {
            "type": ["string", "null"],
            "description": "First characters of the secret, for identification only.",
            "examples": ["whsec_4f..."]
          },
          "status": { "type": "string", "enum": ["active", "inactive", "pending"] }
        }
      },
      "SetWebhookRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTPS endpoint. Private/loopback/metadata addresses are rejected."
          }
        }
      },
      "UpdateWebhookRequest": {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" }
        }
      },
      "WebhookWriteResponse": {
        "type": "object",
        "required": ["status", "webhook_url"],
        "properties": {
          "status": { "type": "string", "const": "success" },
          "webhook_url": { "type": "string", "format": "uri" }
        }
      },
      "SimpleStatusResponse": {
        "type": "object",
        "required": ["status", "message"],
        "properties": {
          "status": { "type": "string", "examples": ["success"] },
          "message": { "type": "string" }
        }
      },
      "Customer": {
        "type": "object",
        "required": ["id"],
        "properties": {
          "id": { "type": "string", "examples": ["cus_c1a2d94k3mx7"] },
          "name": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"], "format": "email" }
        }
      },
      "OrderItemOption": {
        "type": "object",
        "required": ["name", "price", "quantity"],
        "properties": {
          "name": { "type": "string", "examples": ["Extra cheese"] },
          "price": { "type": "integer", "description": "Minor units, per unit." },
          "quantity": { "type": "integer", "minimum": 1 },
          "external_id": {
            "type": ["string", "null"],
            "description": "The `external_id` **you** pushed for this add-on — `null` if it was not synced by your app."
          }
        }
      },
      "OrderItem": {
        "type": "object",
        "required": ["product_id", "product_name", "quantity", "unit_price", "subtotal"],
        "properties": {
          "product_id": { "type": "string", "description": "Quiqqy internal product id.", "examples": ["prd_aa11e0b2c9d4"] },
          "product_variant_id": { "type": ["string", "null"] },
          "product_name": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1 },
          "unit_price": { "type": "integer", "description": "Minor units." },
          "subtotal": { "type": "integer", "description": "Minor units, including options." },
          "tax_amount": { "type": "integer", "description": "Minor units." },
          "external_id": {
            "type": ["string", "null"],
            "description": "The stable id **you** pushed for this item. Scoped to your app (`external_source = app-<your_app_id>`); `null` for items synced by another integration — fall back to `product_name`. **Map order lines off this field, not `product_id`.**"
          },
          "external_source": { "type": ["string", "null"], "examples": ["app-app_2c9d7fk1qz84"] },
          "variant_external_id": {
            "type": ["string", "null"],
            "description": "The variant `external_id` you pushed; `null` for non-variant lines."
          },
          "variant_external_source": { "type": ["string", "null"] },
          "special_instructions": { "type": ["string", "null"] },
          "options": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OrderItemOption" },
            "description": "Selected add-ons / modifiers."
          }
        }
      },
      "Order": {
        "type": "object",
        "description": "Full order payload, delivered in webhook `data`. All money fields are integers in minor units of `currency`.",
        "required": ["id", "order_number", "fulfillment_type", "status", "payment_status", "currency", "subtotal", "total_amount", "created_at", "items"],
        "properties": {
          "id": { "type": "string", "examples": ["ord_7d4e2k91mqx0"] },
          "order_number": { "type": "string", "description": "Human-friendly order number, safe to print on tickets.", "examples": ["ORD-20260730-0001"] },
          "fulfillment_type": {
            "type": "string",
            "enum": ["delivery", "pickup", "dine_in"],
            "description": "How the order is fulfilled."
          },
          "order_type": {
            "type": "string",
            "enum": ["delivery", "pickup", "dine_in"],
            "deprecated": true,
            "description": "Deprecated alias of `fulfillment_type`, kept for v1 handler compatibility."
          },
          "status": { "$ref": "#/components/schemas/OrderStatus" },
          "payment_status": {
            "type": "string",
            "enum": ["pending", "paid", "failed", "refunded"]
          },
          "currency": { "type": "string", "description": "ISO 4217 code.", "examples": ["USD"] },
          "subtotal": { "type": "integer", "description": "Minor units." },
          "tax": { "type": "integer", "description": "Minor units." },
          "delivery_fee": { "type": "integer", "description": "Minor units." },
          "discount": { "type": "integer", "description": "Minor units." },
          "tip": { "type": "integer", "description": "Minor units." },
          "total_amount": { "type": "integer", "description": "Minor units — what the customer pays." },
          "prices_include_tax": { "type": "boolean" },
          "special_instructions": { "type": ["string", "null"] },
          "cancellation_reason": { "type": ["string", "null"], "description": "Set on cancelled orders." },
          "cancellation_reason_code": {
            "oneOf": [
              { "$ref": "#/components/schemas/CancelReasonCode" },
              { "type": "null" }
            ]
          },
          "customer_id": { "type": ["string", "null"] },
          "customer": {
            "oneOf": [
              { "$ref": "#/components/schemas/Customer" },
              { "type": "null" }
            ]
          },
          "delivery_address": {
            "oneOf": [
              { "$ref": "#/components/schemas/Address" },
              { "type": "null" }
            ],
            "description": "Present on delivery orders."
          },
          "table_external_id": {
            "type": ["string", "null"],
            "description": "For dine-in orders placed via table QR — your table `external_id`."
          },
          "estimated_prep_time_minutes": { "type": ["integer", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OrderItem" }
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Common envelope for every webhook delivery. Signature/delivery rules: `X-Quiqqy-Signature` = HMAC-SHA256(webhook_secret, raw body) lowercase hex; 10 s timeout; retries at 1m/5m/30m/2h/6h (6 attempts); dedupe on `event_id`.",
        "required": ["event_type", "event_id", "store_id", "data", "timestamp"],
        "properties": {
          "event_type": { "type": "string", "examples": ["order.created"] },
          "event_id": { "type": "string", "description": "Unique event id — equals `X-Quiqqy-Delivery-Id`. Dedupe on it." },
          "store_id": { "type": ["string", "null"], "description": "`null` on test deliveries." },
          "location_id": {
            "type": ["string", "null"],
            "description": "Your `external_store_id`; `\"test-location\"` on test deliveries."
          },
          "data": { "type": "object", "additionalProperties": true },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "OrderEventEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": {
                "type": "string",
                "enum": ["order.created", "order.updated", "order.cancelled", "order.delivered"]
              },
              "data": { "$ref": "#/components/schemas/Order" }
            }
          }
        ]
      },
      "MenuUpdatedEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "menu.updated" },
              "data": {
                "type": "object",
                "properties": {
                  "menu_external_id": { "type": ["string", "null"] },
                  "changed": {
                    "type": "array",
                    "items": { "type": "string", "enum": ["menus", "categories", "items", "add_ons", "addon_groups"] }
                  },
                  "source": { "type": "string", "examples": ["merchant_portal"] }
                }
              }
            }
          }
        ]
      },
      "ItemAvailabilityEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "menu.item_availability_changed" },
              "data": {
                "type": "object",
                "required": ["items"],
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["product_id", "available"],
                      "properties": {
                        "product_id": { "type": "string" },
                        "external_id": { "type": ["string", "null"] },
                        "available": { "type": "boolean" }
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "StoreStatusEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "store.status_changed" },
              "data": {
                "type": "object",
                "required": ["status"],
                "properties": {
                  "status": { "$ref": "#/components/schemas/StoreStatus" },
                  "previous_status": { "$ref": "#/components/schemas/StoreStatus" },
                  "changed_by": { "type": "string", "examples": ["merchant_portal"] }
                }
              }
            }
          }
        ]
      },
      "ActionRequiredEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/WebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "action_required" },
              "data": {
                "type": "object",
                "required": ["action"],
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": ["menu_push", "resync"],
                    "description": "The command. Also mirrored in the `X-Quiqqy-Action` header."
                  },
                  "request_type": {
                    "type": "string",
                    "enum": ["menu_push", "resync"],
                    "deprecated": true,
                    "description": "Legacy alias of `action`."
                  },
                  "app_id": { "type": "string" },
                  "integration_id": { "type": "string" },
                  "requested_by": { "type": "string", "examples": ["developer_portal"] }
                }
              }
            }
          }
        ]
      }
    }
  }
}
