{
  "openapi": "3.1.0",
  "info": {
    "title": "Quiqqy Hub Online Ordering API",
    "version": "2.0.0",
    "summary": "Build ordering experiences on top of a Quiqqy Hub community: browse the multi-store catalogue, place orders, and react to order events.",
    "description": "A **Hub community** is a marketplace where several merchant stores sell under one brand — a food hall, a residential community, a corporate campus. The community owns its own catalogue mirror, customers, orders, and settlement ledger, and forwards each order to the merchant store that fulfils it.\n\nThe **Hub Online Ordering API** is for **community app builders**: teams building a custom storefront, kiosk, chat-commerce bot, or operations tool on top of a community. It gives you:\n\n- **Community catalogue** — every member store with its categories, items, live availability, and opening hours, mirrored from each merchant's POS.\n- **Item management** — snooze / un-snooze items (86ing) and set store hours for community-level overrides.\n- **Orders** — place orders spanning multiple stores in one basket. Quiqqy **re-derives all prices server-side** from the synced catalogue, splits the basket into one **child order per store**, and forwards each child to its merchant. Track everything through the parent order.\n- **Webhooks** — `order.created` and `order.status_changed` events, HMAC-signed, with delivery logs and a test sender.\n\n## Tenancy\n\nEvery route is scoped by community: `/communities/{community_id}/...`. The community in the path must match the community your token was issued for — cross-tenant requests are rejected with `404`.\n\n## Authentication\n\nAll endpoints require a **partner access token** — a JWT obtained with your app's client credentials through the OAuth 2.0 client flow:\n\n1. Create an app in the Quiqqy developer portal and request Hub access for your community.\n2. Exchange `client_id` / `client_secret` at `POST /oauth2/token` (`grant_type: client_credentials`, `audience: partner-community`).\n3. Send `Authorization: Bearer <access_token>` on every call; tokens expire — cache until `expires_in` and refresh.\n\n## Order lifecycle\n\n```\npending → confirmed → preparing → ready → out_for_delivery → delivered\n                                   └── pickup / dine-in orders end at ready → delivered (completed)\nany non-terminal state → cancelled\n```\n\n## Conventions\n\n- All monetary amounts are **integers in minor units** of the community currency (e.g. `74250` = ₹742.50 or $742.50 depending on currency).\n- All fields are `snake_case`.\n- Ids are opaque strings: `com_...` communities, `st_...` stores, `ord_...` orders, `itm_...` items, `mod_...` modifiers, `wh_...` webhooks.\n- Errors: `{ \"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).",
    "contact": {
      "name": "Quiqqy Developer Platform",
      "url": "https://developers.quiqqy.ai",
      "email": "developers@quiqqy.ai"
    },
    "termsOfService": "https://developers.quiqqy.ai/terms"
  },
  "servers": [
    {
      "url": "https://hub-api.quiqqy.ai/api/v2",
      "description": "Production"
    },
    {
      "url": "https://sandbox.quiqqy.ai/api/v2",
      "description": "Sandbox — test community with seeded stores and catalogue"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Catalogue",
      "description": "The community's multi-store catalogue: stores, categories, items with live availability and hours."
    },
    {
      "name": "Items",
      "description": "Community-level item management: availability and snoozing (86ing)."
    },
    {
      "name": "Stores",
      "description": "Member-store configuration visible to the community, such as opening hours."
    },
    {
      "name": "Orders",
      "description": "Place and track community orders. Multi-store baskets split into one child order per store."
    },
    {
      "name": "Webhooks",
      "description": "Webhook subscription CRUD, test deliveries, and delivery logs."
    }
  ],
  "paths": {
    "/communities/{community_id}/catalogue": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" }
      ],
      "get": {
        "operationId": "getCommunityCatalogue",
        "tags": ["Catalogue"],
        "summary": "Get the community catalogue",
        "description": "Returns the community's full catalogue in one call: member **stores** (with open state and hours), **categories**, and **items** with live availability, prices in minor units, and modifier groups. The catalogue is a mirror of each merchant's POS catalogue, kept in sync by Quiqqy — prices here are exactly what order placement will charge.\n\nUse `store_id` to fetch a single store's slice, and `updated_since` for incremental refreshes.",
        "parameters": [
          {
            "name": "store_id",
            "in": "query",
            "required": false,
            "description": "Limit the catalogue to one member store.",
            "schema": { "type": "string", "examples": ["st_6a1f0b2e9c3d"] }
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "description": "Only return items changed since this timestamp (incremental sync).",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "include_unavailable",
            "in": "query",
            "required": false,
            "description": "Include items currently snoozed or out of stock (default `true`; they carry `available: false`).",
            "schema": { "type": "boolean", "default": true }
          }
        ],
        "responses": {
          "200": {
            "description": "The community catalogue.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Catalogue" },
                "example": {
                  "community_id": "com_8f14e45fce9a",
                  "name": "Cayibel",
                  "currency": "INR",
                  "updated_at": "2026-07-30T08:15:00.000Z",
                  "stores": [
                    {
                      "store_id": "st_6a1f0b2e9c3d",
                      "community_store_id": "cst_d4e5f6a70b1c",
                      "name": "Cayibel Kochi",
                      "description": "Kerala kitchen",
                      "image_url": "https://cdn.quiqqy.ai/stores/kochi.jpg",
                      "status": "active",
                      "is_open": true,
                      "is_featured": true,
                      "category": "South Indian",
                      "hours": {
                        "timezone": "Asia/Kolkata",
                        "weekly": {
                          "monday": [{ "open": "09:00", "close": "22:00" }],
                          "tuesday": [{ "open": "09:00", "close": "22:00" }]
                        }
                      }
                    }
                  ],
                  "categories": [
                    { "category_id": "cat_9b8c7d6e5f4a", "store_id": "st_6a1f0b2e9c3d", "name": "Curries", "sort_order": 1 }
                  ],
                  "items": [
                    {
                      "item_id": "itm_9f8e7d6c5b4a",
                      "external_id": "SKU-FISHCURRY",
                      "store_id": "st_6a1f0b2e9c3d",
                      "category_id": "cat_9b8c7d6e5f4a",
                      "name": "Kerala Fish Curry",
                      "description": "Seer fish in roasted coconut gravy",
                      "price": 32000,
                      "currency": "INR",
                      "image_url": "https://cdn.quiqqy.ai/items/fish-curry.jpg",
                      "available": true,
                      "snoozed_until": null,
                      "modifier_groups": [
                        {
                          "group_id": "grp_1a2b3c4d5e6f",
                          "name": "Spice level",
                          "min_select": 1,
                          "max_select": 1,
                          "required": true,
                          "modifiers": [
                            { "modifier_id": "mod_2b3c4d5e6f7a", "name": "Mild", "price": 0, "available": true },
                            { "modifier_id": "mod_3c4d5e6f7a8b", "name": "Medium", "price": 0, "available": true },
                            { "modifier_id": "mod_4d5e6f7a8b9c", "name": "Fiery", "price": 0, "available": true }
                          ]
                        }
                      ],
                      "updated_at": "2026-07-29T18:40:12.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" }
        }
      }
    },
    "/communities/{community_id}/catalogue/items/{item_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/ItemId" }
      ],
      "get": {
        "operationId": "getCatalogueItem",
        "tags": ["Catalogue"],
        "summary": "Get a catalogue item",
        "description": "Returns a single catalogue item with full detail: price in minor units, availability and snooze state, modifier groups, and the store it belongs to.",
        "responses": {
          "200": {
            "description": "The item.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CatalogueItem" },
                "example": {
                  "item_id": "itm_9f8e7d6c5b4a",
                  "external_id": "SKU-FISHCURRY",
                  "store_id": "st_6a1f0b2e9c3d",
                  "category_id": "cat_9b8c7d6e5f4a",
                  "name": "Kerala Fish Curry",
                  "description": "Seer fish in roasted coconut gravy",
                  "price": 32000,
                  "currency": "INR",
                  "image_url": "https://cdn.quiqqy.ai/items/fish-curry.jpg",
                  "available": true,
                  "snoozed_until": null,
                  "modifier_groups": [
                    {
                      "group_id": "grp_1a2b3c4d5e6f",
                      "name": "Spice level",
                      "min_select": 1,
                      "max_select": 1,
                      "required": true,
                      "modifiers": [
                        { "modifier_id": "mod_2b3c4d5e6f7a", "name": "Mild", "price": 0, "available": true },
                        { "modifier_id": "mod_3c4d5e6f7a8b", "name": "Medium", "price": 0, "available": true }
                      ]
                    }
                  ],
                  "updated_at": "2026-07-29T18:40:12.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" }
        }
      },
      "patch": {
        "operationId": "updateCatalogueItem",
        "tags": ["Items"],
        "summary": "Update item availability / snooze",
        "description": "Community-level 86ing. Set `available: false` to take an item off sale indefinitely, or set `snoozed_until` to take it off sale until a specific time (it reactivates automatically). Setting `available: true` clears any snooze.\n\nThis flag is layered **on top of** the merchant's own availability: an item is orderable only when both the merchant mirror and the community flag say it is.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateCatalogueItemRequest" },
              "example": {
                "available": false,
                "snoozed_until": "2026-07-30T18:00:00.000Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated item.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CatalogueItem" },
                "example": {
                  "item_id": "itm_9f8e7d6c5b4a",
                  "external_id": "SKU-FISHCURRY",
                  "store_id": "st_6a1f0b2e9c3d",
                  "category_id": "cat_9b8c7d6e5f4a",
                  "name": "Kerala Fish Curry",
                  "price": 32000,
                  "currency": "INR",
                  "available": false,
                  "snoozed_until": "2026-07-30T18:00:00.000Z",
                  "modifier_groups": [],
                  "updated_at": "2026-07-30T11:05: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" }
        }
      }
    },
    "/communities/{community_id}/stores/{store_id}/hours": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/HubStoreId" }
      ],
      "put": {
        "operationId": "setStoreHours",
        "tags": ["Stores"],
        "summary": "Set store hours",
        "description": "Sets the member store's opening hours **as seen by this community's storefront**. Weekly hours are per weekday with one or more open/close ranges; `overrides` handle specific dates (holidays, closures). Ordering is blocked outside these hours and the store shows as closed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/StoreHours" },
              "example": {
                "timezone": "Asia/Kolkata",
                "weekly": {
                  "monday": [{ "open": "09:00", "close": "22:00" }],
                  "tuesday": [{ "open": "09:00", "close": "22:00" }],
                  "wednesday": [{ "open": "09:00", "close": "22:00" }],
                  "thursday": [{ "open": "09:00", "close": "22:00" }],
                  "friday": [{ "open": "09:00", "close": "23:00" }],
                  "saturday": [{ "open": "10:00", "close": "23:00" }],
                  "sunday": []
                },
                "overrides": [
                  { "date": "2026-08-15", "closed": true, "reason": "Independence Day" }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hours saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "store_id", "hours"],
                  "properties": {
                    "status": { "type": "string", "const": "success" },
                    "store_id": { "type": "string" },
                    "hours": { "$ref": "#/components/schemas/StoreHours" }
                  }
                },
                "example": {
                  "status": "success",
                  "store_id": "st_6a1f0b2e9c3d",
                  "hours": {
                    "timezone": "Asia/Kolkata",
                    "weekly": {
                      "monday": [{ "open": "09:00", "close": "22:00" }],
                      "sunday": []
                    },
                    "overrides": [
                      { "date": "2026-08-15", "closed": true, "reason": "Independence Day" }
                    ]
                  }
                }
              }
            }
          },
          "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" }
        }
      }
    },
    "/communities/{community_id}/orders": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" }
      ],
      "post": {
        "operationId": "createCommunityOrder",
        "tags": ["Orders"],
        "summary": "Place a community order",
        "description": "Places an order on behalf of a customer. Items may span **several stores** in one basket — Quiqqy splits the order and forwards **one child order per store** to the fulfilling merchant; you track everything through the parent.\n\n**Prices are re-derived server-side** from the community's synced catalogue: any `unit_price` you send is ignored, so a tampered client can never change what a shopper is charged. If a line's live price no longer matches the catalogue snapshot your client displayed, pass `expected_total` and Quiqqy rejects the order with `409 order.price_changed` instead of silently charging a different amount.\n\nSupply an `Idempotency-Key` header to make retries safe — the same key returns the original order instead of creating a duplicate.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique key per order attempt. Retries with the same key return the original result.",
            "schema": { "type": "string", "examples": ["9f2c1e8a-7b6d-4c5e-8f9a-0b1c2d3e4f5a"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PlaceOrderRequest" },
              "example": {
                "fulfillment_type": "delivery",
                "customer": {
                  "name": "Anita R.",
                  "email": "anita@example.com",
                  "phone": "+919812345678"
                },
                "delivery_address": {
                  "street_address": "14 Marine Drive, Apt 6C",
                  "city": "Kochi",
                  "state": "KL",
                  "country": "IN",
                  "zip": "682031"
                },
                "items": [
                  {
                    "item_id": "itm_9f8e7d6c5b4a",
                    "store_id": "st_6a1f0b2e9c3d",
                    "quantity": 2,
                    "modifiers": [
                      { "modifier_id": "mod_3c4d5e6f7a8b", "quantity": 1 }
                    ],
                    "special_instructions": "Less salt please"
                  },
                  {
                    "item_id": "itm_5c4b3a2d1e0f",
                    "store_id": "st_2b7c8d9e0f1a",
                    "quantity": 1
                  }
                ],
                "tip": 2000,
                "coupon_code": "WELCOME10",
                "special_instructions": "Call on arrival",
                "expected_total": 74250,
                "payment": {
                  "method": "card",
                  "provider": "stripe"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order placed. The parent order carries one `child_orders[]` entry per fulfilling store.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HubOrder" },
                "example": {
                  "order_id": "ord_3e5f7a9b1c2d",
                  "order_number": "CQ-10428",
                  "community_id": "com_8f14e45fce9a",
                  "status": "pending",
                  "fulfillment_type": "delivery",
                  "payment_status": "pending",
                  "currency": "INR",
                  "subtotal": 68000,
                  "tax": 3400,
                  "delivery_fee": 4000,
                  "discount": 6800,
                  "tip": 2000,
                  "total_amount": 74250,
                  "coupon_code": "WELCOME10",
                  "special_instructions": "Call on arrival",
                  "customer": {
                    "customer_id": "cus_c7d8e9f01a2b",
                    "name": "Anita R.",
                    "email": "anita@example.com",
                    "phone": "+919812345678"
                  },
                  "delivery_address": {
                    "street_address": "14 Marine Drive, Apt 6C",
                    "city": "Kochi",
                    "state": "KL",
                    "country": "IN",
                    "zip": "682031"
                  },
                  "items": [
                    {
                      "line_id": "lin_1a0b9c8d7e6f",
                      "item_id": "itm_9f8e7d6c5b4a",
                      "store_id": "st_6a1f0b2e9c3d",
                      "name": "Kerala Fish Curry",
                      "quantity": 2,
                      "unit_price": 32000,
                      "subtotal": 64000,
                      "modifiers": [
                        { "modifier_id": "mod_3c4d5e6f7a8b", "name": "Medium", "price": 0, "quantity": 1 }
                      ],
                      "special_instructions": "Less salt please"
                    },
                    {
                      "line_id": "lin_2b1c0d9e8f7a",
                      "item_id": "itm_5c4b3a2d1e0f",
                      "store_id": "st_2b7c8d9e0f1a",
                      "name": "Sulaimani Tea",
                      "quantity": 1,
                      "unit_price": 4000,
                      "subtotal": 4000,
                      "modifiers": []
                    }
                  ],
                  "child_orders": [
                    {
                      "order_id": "ord_4f6a8b0c2d3e",
                      "store_id": "st_6a1f0b2e9c3d",
                      "store_name": "Cayibel Kochi",
                      "status": "pending",
                      "subtotal": 64000,
                      "line_ids": ["lin_1a0b9c8d7e6f"]
                    },
                    {
                      "order_id": "ord_5a7b9c1d3e4f",
                      "store_id": "st_2b7c8d9e0f1a",
                      "store_name": "Cayibel Chai Bar",
                      "status": "pending",
                      "subtotal": 4000,
                      "line_ids": ["lin_2b1c0d9e8f7a"]
                    }
                  ],
                  "created_at": "2026-07-30T09:14:22.000Z",
                  "updated_at": "2026-07-30T09:14:22.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "The community, a store, or an item does not exist in this community's catalogue.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "catalogue.item_not_found",
                    "message": "Item itm_9f8e7d6c5b4a is not in the catalogue of community com_8f14e45fce9a."
                  }
                }
              }
            }
          },
          "409": {
            "description": "The order cannot be placed as requested — a live price differs from `expected_total`, an item is snoozed, or a store is closed.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "price_changed": {
                    "summary": "Server-derived total differs from expected_total",
                    "value": {
                      "error": {
                        "code": "order.price_changed",
                        "message": "The current total is 76250, not 74250. Refresh the catalogue and re-present the basket."
                      }
                    }
                  },
                  "item_unavailable": {
                    "summary": "An item is snoozed or out of stock",
                    "value": {
                      "error": {
                        "code": "order.item_unavailable",
                        "message": "Item itm_9f8e7d6c5b4a is currently unavailable."
                      }
                    }
                  },
                  "store_closed": {
                    "summary": "A fulfilling store is closed",
                    "value": {
                      "error": {
                        "code": "order.store_closed",
                        "message": "Store st_6a1f0b2e9c3d is closed and cannot accept orders right now."
                      }
                    }
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      },
      "get": {
        "operationId": "listCommunityOrders",
        "tags": ["Orders"],
        "summary": "List community orders",
        "description": "Lists orders in the community, newest first. Filter by status, store, or date range.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": { "$ref": "#/components/schemas/HubOrderStatus" }
          },
          {
            "name": "store_id",
            "in": "query",
            "required": false,
            "description": "Only orders with at least one child order at this store.",
            "schema": { "type": "string" }
          },
          {
            "name": "date_from",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "date_to",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of orders.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HubOrderList" },
                "example": {
                  "data": [
                    {
                      "order_id": "ord_3e5f7a9b1c2d",
                      "order_number": "CQ-10428",
                      "community_id": "com_8f14e45fce9a",
                      "status": "preparing",
                      "fulfillment_type": "delivery",
                      "payment_status": "completed",
                      "currency": "INR",
                      "subtotal": 68000,
                      "tax": 3400,
                      "delivery_fee": 4000,
                      "discount": 6800,
                      "tip": 2000,
                      "total_amount": 74250,
                      "customer": {
                        "customer_id": "cus_c7d8e9f01a2b",
                        "name": "Anita R."
                      },
                      "items": [],
                      "child_orders": [
                        { "order_id": "ord_4f6a8b0c2d3e", "store_id": "st_6a1f0b2e9c3d", "store_name": "Cayibel Kochi", "status": "preparing", "subtotal": 64000, "line_ids": ["lin_1a0b9c8d7e6f"] }
                      ],
                      "created_at": "2026-07-30T09:14:22.000Z",
                      "updated_at": "2026-07-30T09:24:02.000Z"
                    }
                  ],
                  "page": 1,
                  "limit": 20,
                  "total": 128
                }
              }
            }
          },
          "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" }
        }
      }
    },
    "/communities/{community_id}/orders/{order_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/HubOrderId" }
      ],
      "get": {
        "operationId": "getCommunityOrder",
        "tags": ["Orders"],
        "summary": "Get an order",
        "description": "Returns the full parent order, including every line, the customer, and per-store `child_orders[]` with their individual statuses. The parent `status` is derived from the children: it advances when all children have advanced, and `out_for_delivery`/`delivered` reflect the delivery leg.",
        "responses": {
          "200": {
            "description": "The order.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HubOrder" },
                "example": {
                  "order_id": "ord_3e5f7a9b1c2d",
                  "order_number": "CQ-10428",
                  "community_id": "com_8f14e45fce9a",
                  "status": "preparing",
                  "fulfillment_type": "delivery",
                  "payment_status": "completed",
                  "currency": "INR",
                  "subtotal": 68000,
                  "tax": 3400,
                  "delivery_fee": 4000,
                  "discount": 6800,
                  "tip": 2000,
                  "total_amount": 74250,
                  "customer": {
                    "customer_id": "cus_c7d8e9f01a2b",
                    "name": "Anita R.",
                    "email": "anita@example.com",
                    "phone": "+919812345678"
                  },
                  "items": [
                    {
                      "line_id": "lin_1a0b9c8d7e6f",
                      "item_id": "itm_9f8e7d6c5b4a",
                      "store_id": "st_6a1f0b2e9c3d",
                      "name": "Kerala Fish Curry",
                      "quantity": 2,
                      "unit_price": 32000,
                      "subtotal": 64000,
                      "modifiers": [
                        { "modifier_id": "mod_3c4d5e6f7a8b", "name": "Medium", "price": 0, "quantity": 1 }
                      ]
                    }
                  ],
                  "child_orders": [
                    {
                      "order_id": "ord_4f6a8b0c2d3e",
                      "store_id": "st_6a1f0b2e9c3d",
                      "store_name": "Cayibel Kochi",
                      "status": "preparing",
                      "subtotal": 64000,
                      "line_ids": ["lin_1a0b9c8d7e6f"]
                    }
                  ],
                  "created_at": "2026-07-30T09:14:22.000Z",
                  "updated_at": "2026-07-30T09:24:02.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" }
        }
      }
    },
    "/communities/{community_id}/orders/{order_id}/cancel": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/HubOrderId" }
      ],
      "post": {
        "operationId": "cancelCommunityOrder",
        "tags": ["Orders"],
        "summary": "Cancel an order",
        "description": "Cancels the parent order and every child that has not yet reached a terminal state. Cancellation is only possible while no child is `out_for_delivery` or later. Emits `order.status_changed` to all subscribed webhooks; paid orders move to `payment_status: refunded` once the refund settles.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CancelHubOrderRequest" },
              "example": {
                "reason": "Customer requested cancellation",
                "reason_code": "OTHER"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order cancelled.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HubOrder" },
                "example": {
                  "order_id": "ord_3e5f7a9b1c2d",
                  "order_number": "CQ-10428",
                  "community_id": "com_8f14e45fce9a",
                  "status": "cancelled",
                  "fulfillment_type": "delivery",
                  "payment_status": "refunded",
                  "currency": "INR",
                  "subtotal": 68000,
                  "tax": 3400,
                  "delivery_fee": 4000,
                  "discount": 6800,
                  "tip": 2000,
                  "total_amount": 74250,
                  "cancellation_reason": "Customer requested cancellation",
                  "customer": { "customer_id": "cus_c7d8e9f01a2b", "name": "Anita R." },
                  "items": [],
                  "child_orders": [
                    { "order_id": "ord_4f6a8b0c2d3e", "store_id": "st_6a1f0b2e9c3d", "store_name": "Cayibel Kochi", "status": "cancelled", "subtotal": 64000, "line_ids": ["lin_1a0b9c8d7e6f"] }
                  ],
                  "created_at": "2026-07-30T09:14:22.000Z",
                  "updated_at": "2026-07-30T09:31:40.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "The order can no longer be cancelled (a child is out for delivery or already terminal).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "order.not_cancellable",
                    "message": "Order ord_3e5f7a9b1c2d is out for delivery and can no longer be cancelled."
                  }
                }
              }
            }
          },
          "422": { "$ref": "#/components/responses/UnprocessableEntity" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/communities/{community_id}/webhooks": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" }
      ],
      "get": {
        "operationId": "listWebhooks",
        "tags": ["Webhooks"],
        "summary": "List webhook subscriptions",
        "description": "Lists the community's webhook subscriptions. Secrets are masked.",
        "responses": {
          "200": {
            "description": "Subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Webhook" }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "webhook_id": "wh_9f2c1e8a7b6d",
                      "name": "Ops relay",
                      "url": "https://ops.example.com/hooks/quiqqy",
                      "events": ["order.created", "order.status_changed"],
                      "is_active": true,
                      "secret_masked": "whsec_9f...",
                      "headers": {},
                      "created_at": "2026-06-01T10: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" }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": ["Webhooks"],
        "summary": "Create a webhook subscription",
        "description": "Subscribes an endpoint to community events. Omit `events` (or include `\"*\"`) to receive every event. If you don't supply a `secret`, Quiqqy generates one and returns it **once** in this response — store it to verify `X-Quiqqy-Signature` on deliveries.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateWebhookRequest" },
              "example": {
                "name": "Ops relay",
                "url": "https://ops.example.com/hooks/quiqqy",
                "events": ["order.created", "order.status_changed"],
                "is_active": true,
                "headers": { "X-Env": "production" }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscription created. `secret` is only returned here.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/WebhookWithSecret" },
                "example": {
                  "webhook_id": "wh_9f2c1e8a7b6d",
                  "name": "Ops relay",
                  "url": "https://ops.example.com/hooks/quiqqy",
                  "events": ["order.created", "order.status_changed"],
                  "is_active": true,
                  "secret": "whsec_9f2c1e8a7b6d5c4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e",
                  "headers": { "X-Env": "production" },
                  "created_at": "2026-07-30T10:00:00.000Z"
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "422": {
            "description": "Validation failed — e.g. the URL is not a public HTTPS endpoint, or `events` contains an unknown event.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": {
                    "code": "webhook.invalid_event",
                    "message": "Unknown event type order.deleted. Valid events: order.created, order.status_changed."
                  }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/communities/{community_id}/webhooks/{webhook_id}": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/WebhookId" }
      ],
      "get": {
        "operationId": "getWebhook",
        "tags": ["Webhooks"],
        "summary": "Get a webhook subscription",
        "description": "Returns one subscription. The secret is masked.",
        "responses": {
          "200": {
            "description": "The subscription.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Webhook" },
                "example": {
                  "webhook_id": "wh_9f2c1e8a7b6d",
                  "name": "Ops relay",
                  "url": "https://ops.example.com/hooks/quiqqy",
                  "events": ["order.created", "order.status_changed"],
                  "is_active": true,
                  "secret_masked": "whsec_9f...",
                  "headers": {},
                  "created_at": "2026-06-01T10: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": "updateWebhook",
        "tags": ["Webhooks"],
        "summary": "Update a webhook subscription",
        "description": "Updates the subscription's URL, event filter, active flag, or extra headers. Sending a new `secret` rotates it — old deliveries in flight are still signed with the previous secret.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateWebhookRequest" },
              "example": {
                "url": "https://ops.example.com/hooks/quiqqy/v2",
                "is_active": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated subscription.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Webhook" },
                "example": {
                  "webhook_id": "wh_9f2c1e8a7b6d",
                  "name": "Ops relay",
                  "url": "https://ops.example.com/hooks/quiqqy/v2",
                  "events": ["order.created", "order.status_changed"],
                  "is_active": true,
                  "secret_masked": "whsec_9f...",
                  "headers": {},
                  "created_at": "2026-06-01T10: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": "deleteWebhook",
        "tags": ["Webhooks"],
        "summary": "Delete a webhook subscription",
        "description": "Removes the subscription. Deliveries stop immediately; historical delivery logs are retained.",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "message"],
                  "properties": {
                    "status": { "type": "string", "const": "success" },
                    "message": { "type": "string" }
                  }
                },
                "example": {
                  "status": "success",
                  "message": "Webhook subscription deleted"
                }
              }
            }
          },
          "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" }
        }
      }
    },
    "/communities/{community_id}/webhooks/{webhook_id}/test": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/WebhookId" }
      ],
      "post": {
        "operationId": "testWebhook",
        "tags": ["Webhooks"],
        "summary": "Send a test delivery",
        "description": "Sends a synthetic `order.created` event (payload flagged with `\"test\": true`) to the subscription's URL, signed exactly like a real delivery, and records the attempt in the delivery log. Use it to verify reachability and signature handling before going live — never create a real order from a test payload.",
        "responses": {
          "200": {
            "description": "Test delivery attempted; result recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "delivery_id", "response_status"],
                  "properties": {
                    "status": { "type": "string", "examples": ["ok"] },
                    "delivery_id": { "type": "string", "examples": ["whd_5a4b3c2d1e0f"] },
                    "response_status": { "type": ["integer", "null"], "description": "HTTP status your endpoint returned, or null on timeout / network error." },
                    "duration_ms": { "type": ["integer", "null"] }
                  }
                },
                "example": {
                  "status": "ok",
                  "delivery_id": "whd_5a4b3c2d1e0f",
                  "response_status": 200,
                  "duration_ms": 182
                }
              }
            }
          },
          "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" }
        }
      }
    },
    "/communities/{community_id}/webhooks/{webhook_id}/deliveries": {
      "parameters": [
        { "$ref": "#/components/parameters/CommunityId" },
        { "$ref": "#/components/parameters/WebhookId" }
      ],
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": ["Webhooks"],
        "summary": "List delivery attempts",
        "description": "Delivery log for a subscription, newest first: event type, attempt number, your endpoint's response status, and the first 500 characters of the response body. Use it to debug 4xx/5xx handlers and timeouts. `response_status` 2xx counts as success; anything else (including 3xx and network errors) is a failure that entered the retry ladder.",
        "parameters": [
          {
            "name": "success",
            "in": "query",
            "required": false,
            "description": "Filter to successful (`true`) or failed (`false`) attempts.",
            "schema": { "type": "boolean" }
          },
          {
            "name": "event",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["order.created", "order.status_changed"] }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of delivery attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data", "total"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/WebhookDelivery" }
                    },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "total": { "type": "integer" }
                  }
                },
                "example": {
                  "data": [
                    {
                      "delivery_id": "whd_5a4b3c2d1e0f",
                      "webhook_id": "wh_9f2c1e8a7b6d",
                      "event": "order.status_changed",
                      "event_id": "evt_6b5c4d3e2f1a",
                      "attempt": 1,
                      "success": true,
                      "response_status": 200,
                      "response_body": "{\"ok\":true}",
                      "delivered_at": "2026-07-30T09:24:03.000Z",
                      "duration_ms": 145
                    },
                    {
                      "delivery_id": "whd_6b5c4d3e2f1a",
                      "webhook_id": "wh_9f2c1e8a7b6d",
                      "event": "order.created",
                      "event_id": "evt_7c6d5e4f3a2b",
                      "attempt": 2,
                      "success": false,
                      "response_status": 500,
                      "response_body": "Internal Server Error",
                      "delivered_at": "2026-07-30T09:15:24.000Z",
                      "duration_ms": 2210
                    }
                  ],
                  "page": 1,
                  "limit": 20,
                  "total": 342
                }
              }
            }
          },
          "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": "webhookHubOrderCreated",
        "tags": ["Webhooks"],
        "summary": "order.created — a community order was placed",
        "description": "Sent when an order is placed in the community — through your app, the community storefront, or an operator. `data.order` carries the full parent order including `child_orders[]`.\n\n**Signing & delivery (all events):** every delivery carries `X-Quiqqy-Signature` — HMAC-SHA256 of the **raw request body** using the subscription's `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/HubOrderCreatedEnvelope" },
              "example": {
                "event_type": "order.created",
                "event_id": "evt_7c6d5e4f3a2b",
                "community_id": "com_8f14e45fce9a",
                "timestamp": "2026-07-30T09:14:22.108Z",
                "data": {
                  "order": {
                    "order_id": "ord_3e5f7a9b1c2d",
                    "order_number": "CQ-10428",
                    "community_id": "com_8f14e45fce9a",
                    "status": "pending",
                    "fulfillment_type": "delivery",
                    "payment_status": "pending",
                    "currency": "INR",
                    "subtotal": 68000,
                    "tax": 3400,
                    "delivery_fee": 4000,
                    "discount": 6800,
                    "tip": 2000,
                    "total_amount": 74250,
                    "customer": {
                      "customer_id": "cus_c7d8e9f01a2b",
                      "name": "Anita R.",
                      "phone": "+919812345678"
                    },
                    "items": [
                      {
                        "line_id": "lin_1a0b9c8d7e6f",
                        "item_id": "itm_9f8e7d6c5b4a",
                        "store_id": "st_6a1f0b2e9c3d",
                        "name": "Kerala Fish Curry",
                        "quantity": 2,
                        "unit_price": 32000,
                        "subtotal": 64000,
                        "modifiers": [
                          { "modifier_id": "mod_3c4d5e6f7a8b", "name": "Medium", "price": 0, "quantity": 1 }
                        ]
                      }
                    ],
                    "child_orders": [
                      {
                        "order_id": "ord_4f6a8b0c2d3e",
                        "store_id": "st_6a1f0b2e9c3d",
                        "store_name": "Cayibel Kochi",
                        "status": "pending",
                        "subtotal": 64000,
                        "line_ids": ["lin_1a0b9c8d7e6f"]
                      }
                    ],
                    "created_at": "2026-07-30T09:14:22.000Z",
                    "updated_at": "2026-07-30T09:14:22.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery. Non-2xx (including 3xx) triggers the retry ladder."
          }
        }
      }
    },
    "order.status_changed": {
      "post": {
        "operationId": "webhookHubOrderStatusChanged",
        "tags": ["Webhooks"],
        "summary": "order.status_changed — an order advanced",
        "description": "Sent every time the parent order's status changes along `pending → confirmed → preparing → ready → out_for_delivery → delivered` (or to `cancelled` from any non-terminal state). `data.previous_status` and `data.order.status` give you the transition; `data.order.child_orders[]` carries each store's current state so you can render per-store progress. 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/HubOrderStatusChangedEnvelope" },
              "example": {
                "event_type": "order.status_changed",
                "event_id": "evt_8d7e6f5a4b3c",
                "community_id": "com_8f14e45fce9a",
                "timestamp": "2026-07-30T09:24:02.451Z",
                "data": {
                  "previous_status": "confirmed",
                  "order": {
                    "order_id": "ord_3e5f7a9b1c2d",
                    "order_number": "CQ-10428",
                    "community_id": "com_8f14e45fce9a",
                    "status": "preparing",
                    "fulfillment_type": "delivery",
                    "payment_status": "completed",
                    "currency": "INR",
                    "subtotal": 68000,
                    "tax": 3400,
                    "delivery_fee": 4000,
                    "discount": 6800,
                    "tip": 2000,
                    "total_amount": 74250,
                    "customer": {
                      "customer_id": "cus_c7d8e9f01a2b",
                      "name": "Anita R."
                    },
                    "items": [],
                    "child_orders": [
                      {
                        "order_id": "ord_4f6a8b0c2d3e",
                        "store_id": "st_6a1f0b2e9c3d",
                        "store_name": "Cayibel Kochi",
                        "status": "preparing",
                        "subtotal": 64000,
                        "line_ids": ["lin_1a0b9c8d7e6f"]
                      }
                    ],
                    "created_at": "2026-07-30T09:14:22.000Z",
                    "updated_at": "2026-07-30T09:24:02.000Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx within 10 seconds to acknowledge the delivery."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "**Partner access token** (OAuth 2.0 client flow). Exchange your app's `client_id` / `client_secret` at `POST /oauth2/token` with `grant_type: client_credentials` and `audience: partner-community` to receive `{ access_token, expires_in }`. The token is scoped to your community — requests against another `community_id` return `404`. Cache the token until it expires; do not mint one per request."
      }
    },
    "parameters": {
      "CommunityId": {
        "name": "community_id",
        "in": "path",
        "required": true,
        "description": "The community your token was issued for.",
        "schema": { "type": "string", "examples": ["com_8f14e45fce9a"] }
      },
      "ItemId": {
        "name": "item_id",
        "in": "path",
        "required": true,
        "description": "Catalogue item id.",
        "schema": { "type": "string", "examples": ["itm_9f8e7d6c5b4a"] }
      },
      "HubStoreId": {
        "name": "store_id",
        "in": "path",
        "required": true,
        "description": "Member store id.",
        "schema": { "type": "string", "examples": ["st_6a1f0b2e9c3d"] }
      },
      "HubOrderId": {
        "name": "order_id",
        "in": "path",
        "required": true,
        "description": "Parent order id.",
        "schema": { "type": "string", "examples": ["ord_3e5f7a9b1c2d"] }
      },
      "WebhookId": {
        "name": "webhook_id",
        "in": "path",
        "required": true,
        "description": "Webhook subscription id.",
        "schema": { "type": "string", "examples": ["wh_9f2c1e8a7b6d"] }
      },
      "XQuiqqySignature": {
        "name": "X-Quiqqy-Signature",
        "in": "header",
        "required": true,
        "description": "HMAC-SHA256 of the raw request body using the subscription's `secret`, lowercase hex. Verify with a constant-time compare against the raw bytes.",
        "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", "enum": ["order.created", "order.status_changed"] }
      },
      "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_7c6d5e4f3a2b"] }
      },
      "XQuiqqyLocationId": {
        "name": "X-Quiqqy-Location-Id",
        "in": "header",
        "required": false,
        "description": "The community id the event belongs to.",
        "schema": { "type": "string", "examples": ["com_8f14e45fce9a"] }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, expired, or invalid access token.",
        "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 permitted to perform this action.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "auth.forbidden",
                "message": "This token is not permitted to manage webhooks for this community."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist in this community — including cross-tenant requests, which always return 404.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "resource.not_found",
                "message": "The requested resource was not found in this community."
              }
            }
          }
        }
      },
      "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.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": {
                "code": "validation.failed",
                "message": "items[0].quantity must be an integer greater than or equal to 1."
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded. 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`, `catalogue.item_not_found`, `order.price_changed`, `order.not_cancellable`, `webhook.invalid_event`, `validation.failed`, `rate_limit.exceeded`.",
                "examples": ["order.price_changed"]
              },
              "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"] }
        }
      },
      "TimeRange": {
        "type": "object",
        "required": ["open", "close"],
        "properties": {
          "open": { "type": "string", "description": "`HH:MM`, 24-hour, in the store's timezone.", "examples": ["09:00"] },
          "close": { "type": "string", "examples": ["22:00"] }
        }
      },
      "StoreHours": {
        "type": "object",
        "required": ["timezone", "weekly"],
        "properties": {
          "timezone": { "type": "string", "description": "IANA timezone.", "examples": ["Asia/Kolkata"] },
          "weekly": {
            "type": "object",
            "description": "Open/close ranges per weekday. An empty array means closed that day.",
            "properties": {
              "monday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "tuesday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "wednesday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "thursday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "friday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "saturday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } },
              "sunday": { "type": "array", "items": { "$ref": "#/components/schemas/TimeRange" } }
            }
          },
          "overrides": {
            "type": "array",
            "description": "Date-specific exceptions (holidays, special hours).",
            "items": {
              "type": "object",
              "required": ["date"],
              "properties": {
                "date": { "type": "string", "format": "date" },
                "closed": { "type": "boolean", "default": false },
                "ranges": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/TimeRange" },
                  "description": "Special hours for the date (ignored when `closed` is true)."
                },
                "reason": { "type": "string" }
              }
            }
          }
        }
      },
      "CatalogueStore": {
        "type": "object",
        "required": ["store_id", "name", "status", "is_open"],
        "properties": {
          "store_id": { "type": "string", "examples": ["st_6a1f0b2e9c3d"] },
          "community_store_id": { "type": "string", "description": "The store's membership record id inside this community." },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "image_url": { "type": ["string", "null"], "format": "uri" },
          "status": { "type": "string", "enum": ["active", "pending", "inactive"] },
          "is_open": { "type": "boolean", "description": "Live open state, derived from hours + manual pauses." },
          "is_featured": { "type": "boolean" },
          "category": { "type": ["string", "null"], "description": "Merchandising category, e.g. `South Indian`." },
          "hours": {
            "oneOf": [
              { "$ref": "#/components/schemas/StoreHours" },
              { "type": "null" }
            ]
          }
        }
      },
      "CatalogueCategory": {
        "type": "object",
        "required": ["category_id", "store_id", "name"],
        "properties": {
          "category_id": { "type": "string", "examples": ["cat_9b8c7d6e5f4a"] },
          "store_id": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "image_url": { "type": ["string", "null"], "format": "uri" },
          "sort_order": { "type": "integer" }
        }
      },
      "Modifier": {
        "type": "object",
        "required": ["modifier_id", "name", "price"],
        "properties": {
          "modifier_id": { "type": "string", "examples": ["mod_3c4d5e6f7a8b"] },
          "name": { "type": "string", "examples": ["Medium"] },
          "price": { "type": "integer", "description": "Minor units, added per unit of the parent item." },
          "available": { "type": "boolean", "default": true }
        }
      },
      "ModifierGroup": {
        "type": "object",
        "required": ["group_id", "name", "modifiers"],
        "properties": {
          "group_id": { "type": "string", "examples": ["grp_1a2b3c4d5e6f"] },
          "name": { "type": "string", "examples": ["Spice level"] },
          "min_select": { "type": "integer", "minimum": 0 },
          "max_select": { "type": "integer", "minimum": 1 },
          "required": { "type": "boolean" },
          "modifiers": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Modifier" }
          }
        }
      },
      "CatalogueItem": {
        "type": "object",
        "required": ["item_id", "store_id", "name", "price", "currency", "available"],
        "properties": {
          "item_id": { "type": "string", "examples": ["itm_9f8e7d6c5b4a"] },
          "external_id": {
            "type": ["string", "null"],
            "description": "The merchant POS id for this item, when synced from a POS."
          },
          "store_id": { "type": "string" },
          "category_id": { "type": ["string", "null"] },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "price": { "type": "integer", "description": "Minor units. This is the price order placement charges." },
          "currency": { "type": "string", "description": "ISO 4217 code.", "examples": ["INR"] },
          "image_url": { "type": ["string", "null"], "format": "uri" },
          "available": {
            "type": "boolean",
            "description": "Effective orderability: merchant availability AND community snooze combined."
          },
          "snoozed_until": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When set, the item is off sale until this time and reactivates automatically."
          },
          "modifier_groups": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ModifierGroup" }
          },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Catalogue": {
        "type": "object",
        "required": ["community_id", "currency", "stores", "categories", "items"],
        "properties": {
          "community_id": { "type": "string", "examples": ["com_8f14e45fce9a"] },
          "name": { "type": "string" },
          "currency": { "type": "string", "examples": ["INR"] },
          "updated_at": { "type": "string", "format": "date-time" },
          "stores": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CatalogueStore" }
          },
          "categories": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CatalogueCategory" }
          },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CatalogueItem" }
          }
        }
      },
      "UpdateCatalogueItemRequest": {
        "type": "object",
        "description": "At least one of `available` or `snoozed_until` must be sent.",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "`false` takes the item off sale indefinitely; `true` restores it and clears any snooze."
          },
          "snoozed_until": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Take the item off sale until this time (auto-reactivates). `null` clears an existing snooze."
          }
        }
      },
      "HubOrderStatus": {
        "type": "string",
        "enum": ["pending", "confirmed", "preparing", "ready", "out_for_delivery", "delivered", "cancelled"],
        "description": "Lifecycle: `pending → confirmed → preparing → ready → out_for_delivery → delivered`; `cancelled` from any non-terminal state. Pickup and dine-in orders complete from `ready`."
      },
      "PaymentStatus": {
        "type": "string",
        "enum": ["pending", "processing", "completed", "failed", "refunded"]
      },
      "FulfillmentType": {
        "type": "string",
        "enum": ["delivery", "pickup", "dine_in"]
      },
      "OrderCustomerInput": {
        "type": "object",
        "required": ["name"],
        "description": "The shopper this order is for. Quiqqy finds-or-creates the community customer by `email`/`phone`.",
        "properties": {
          "customer_id": { "type": "string", "description": "Pass a known community customer id instead of contact fields." },
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string" }
        }
      },
      "PlaceOrderModifier": {
        "type": "object",
        "required": ["modifier_id"],
        "properties": {
          "modifier_id": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1, "default": 1 }
        }
      },
      "PlaceOrderItem": {
        "type": "object",
        "required": ["item_id", "quantity"],
        "properties": {
          "item_id": { "type": "string", "description": "Catalogue item id. The line's price comes from the catalogue — never from the request." },
          "store_id": {
            "type": "string",
            "description": "The member store fulfilling this line. Optional — inferred from the item — but recommended for explicitness."
          },
          "quantity": { "type": "integer", "minimum": 1 },
          "modifiers": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/PlaceOrderModifier" }
          },
          "special_instructions": { "type": "string" }
        }
      },
      "PaymentInput": {
        "type": "object",
        "required": ["method"],
        "properties": {
          "method": {
            "type": "string",
            "enum": ["card", "digital_wallet", "cash", "bank_transfer"]
          },
          "provider": { "type": "string", "description": "Payment provider handling collection, e.g. `stripe`, `razorpay`.", "examples": ["stripe"] }
        }
      },
      "PlaceOrderRequest": {
        "type": "object",
        "required": ["fulfillment_type", "customer", "items", "payment"],
        "properties": {
          "fulfillment_type": { "$ref": "#/components/schemas/FulfillmentType" },
          "customer": { "$ref": "#/components/schemas/OrderCustomerInput" },
          "delivery_address": {
            "$ref": "#/components/schemas/Address",
            "description": "Required for `delivery` orders."
          },
          "items": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/PlaceOrderItem" },
            "description": "May span multiple stores — Quiqqy creates one child order per store."
          },
          "tip": { "type": "integer", "description": "Minor units.", "default": 0 },
          "coupon_code": { "type": "string" },
          "special_instructions": { "type": "string" },
          "pickup_at": {
            "type": "string",
            "format": "date-time",
            "description": "For scheduled pickup orders — when the customer will collect."
          },
          "expected_total": {
            "type": "integer",
            "description": "Minor units — the total your client displayed. If the server-derived total differs, the order is rejected with `409 order.price_changed` instead of charging a different amount."
          },
          "payment": { "$ref": "#/components/schemas/PaymentInput" }
        }
      },
      "OrderLineModifier": {
        "type": "object",
        "required": ["modifier_id", "name", "price", "quantity"],
        "properties": {
          "modifier_id": { "type": "string" },
          "name": { "type": "string" },
          "price": { "type": "integer", "description": "Minor units, per unit." },
          "quantity": { "type": "integer" }
        }
      },
      "OrderLine": {
        "type": "object",
        "required": ["line_id", "item_id", "store_id", "name", "quantity", "unit_price", "subtotal"],
        "properties": {
          "line_id": { "type": "string", "examples": ["lin_1a0b9c8d7e6f"] },
          "item_id": { "type": "string" },
          "store_id": { "type": "string" },
          "name": { "type": "string" },
          "quantity": { "type": "integer" },
          "unit_price": { "type": "integer", "description": "Minor units — server-derived from the catalogue." },
          "subtotal": { "type": "integer", "description": "Minor units, including modifiers." },
          "modifiers": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OrderLineModifier" }
          },
          "special_instructions": { "type": ["string", "null"] }
        }
      },
      "ChildOrder": {
        "type": "object",
        "description": "One child order per fulfilling store, forwarded to that store's merchant.",
        "required": ["order_id", "store_id", "status", "subtotal", "line_ids"],
        "properties": {
          "order_id": { "type": "string", "examples": ["ord_4f6a8b0c2d3e"] },
          "store_id": { "type": "string" },
          "store_name": { "type": "string" },
          "status": { "$ref": "#/components/schemas/HubOrderStatus" },
          "subtotal": { "type": "integer", "description": "Minor units — this store's share of the basket." },
          "line_ids": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Parent order lines fulfilled by this store."
          }
        }
      },
      "OrderCustomer": {
        "type": "object",
        "required": ["customer_id", "name"],
        "properties": {
          "customer_id": { "type": "string", "examples": ["cus_c7d8e9f01a2b"] },
          "name": { "type": "string" },
          "email": { "type": ["string", "null"], "format": "email" },
          "phone": { "type": ["string", "null"] }
        }
      },
      "HubOrder": {
        "type": "object",
        "description": "A community (parent) order. Money fields are integers in minor units of `currency`.",
        "required": ["order_id", "order_number", "community_id", "status", "fulfillment_type", "payment_status", "currency", "subtotal", "total_amount", "items", "child_orders", "created_at"],
        "properties": {
          "order_id": { "type": "string", "examples": ["ord_3e5f7a9b1c2d"] },
          "order_number": { "type": "string", "examples": ["CQ-10428"] },
          "community_id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/HubOrderStatus" },
          "fulfillment_type": { "$ref": "#/components/schemas/FulfillmentType" },
          "payment_status": { "$ref": "#/components/schemas/PaymentStatus" },
          "currency": { "type": "string", "examples": ["INR"] },
          "subtotal": { "type": "integer" },
          "tax": { "type": "integer" },
          "delivery_fee": { "type": "integer" },
          "discount": { "type": "integer" },
          "tip": { "type": "integer" },
          "total_amount": { "type": "integer" },
          "coupon_code": { "type": ["string", "null"] },
          "special_instructions": { "type": ["string", "null"] },
          "cancellation_reason": { "type": ["string", "null"] },
          "customer": { "$ref": "#/components/schemas/OrderCustomer" },
          "delivery_address": {
            "oneOf": [
              { "$ref": "#/components/schemas/Address" },
              { "type": "null" }
            ]
          },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/OrderLine" }
          },
          "child_orders": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ChildOrder" }
          },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "HubOrderList": {
        "type": "object",
        "required": ["data", "page", "limit", "total"],
        "properties": {
          "data": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/HubOrder" }
          },
          "page": { "type": "integer" },
          "limit": { "type": "integer" },
          "total": { "type": "integer" }
        }
      },
      "CancelHubOrderRequest": {
        "type": "object",
        "required": ["reason"],
        "properties": {
          "reason": { "type": "string", "description": "Customer-facing cancellation reason." },
          "reason_code": {
            "type": "string",
            "enum": ["ITEM_OUT_OF_STOCK", "STORE_CLOSED", "POS_OFFLINE", "CAPACITY", "PRICE_MISMATCH", "OTHER"],
            "description": "Structured reason for automated handling."
          }
        }
      },
      "CreateWebhookRequest": {
        "type": "object",
        "required": ["name", "url"],
        "properties": {
          "name": { "type": "string", "examples": ["Ops relay"] },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTPS endpoint. Private/loopback/metadata addresses are rejected."
          },
          "secret": {
            "type": "string",
            "description": "HMAC signing secret. Omit to have Quiqqy generate one (returned once in the response)."
          },
          "events": {
            "type": "array",
            "items": { "type": "string", "enum": ["order.created", "order.status_changed", "*"] },
            "description": "Events to receive. Omit or include `\"*\"` for all."
          },
          "is_active": { "type": "boolean", "default": true },
          "headers": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Extra headers sent with every delivery."
          }
        }
      },
      "UpdateWebhookRequest": {
        "type": "object",
        "description": "All fields optional; only sent fields are changed.",
        "properties": {
          "name": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "secret": { "type": "string", "description": "Sending a new value rotates the signing secret." },
          "events": {
            "type": "array",
            "items": { "type": "string", "enum": ["order.created", "order.status_changed", "*"] }
          },
          "is_active": { "type": "boolean" },
          "headers": {
            "type": "object",
            "additionalProperties": { "type": "string" }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "required": ["webhook_id", "name", "url", "events", "is_active"],
        "properties": {
          "webhook_id": { "type": "string", "examples": ["wh_9f2c1e8a7b6d"] },
          "name": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "events": {
            "type": "array",
            "items": { "type": "string" }
          },
          "is_active": { "type": "boolean" },
          "secret_masked": { "type": ["string", "null"], "examples": ["whsec_9f..."] },
          "headers": {
            "type": "object",
            "additionalProperties": { "type": "string" }
          },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "WebhookWithSecret": {
        "allOf": [
          { "$ref": "#/components/schemas/Webhook" },
          {
            "type": "object",
            "required": ["secret"],
            "properties": {
              "secret": {
                "type": "string",
                "description": "Full signing secret — only returned on creation. Store it server-side."
              }
            }
          }
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "required": ["delivery_id", "webhook_id", "event", "event_id", "attempt", "success", "delivered_at"],
        "properties": {
          "delivery_id": { "type": "string", "examples": ["whd_5a4b3c2d1e0f"] },
          "webhook_id": { "type": "string" },
          "event": { "type": "string", "examples": ["order.status_changed"] },
          "event_id": { "type": "string" },
          "attempt": { "type": "integer", "minimum": 1, "maximum": 6, "description": "Which attempt on the retry ladder this was (1 = first delivery)." },
          "success": { "type": "boolean" },
          "response_status": { "type": ["integer", "null"], "description": "HTTP status from your endpoint; null on timeout / network error." },
          "response_body": { "type": ["string", "null"], "description": "First 500 characters of your endpoint's response." },
          "delivered_at": { "type": "string", "format": "date-time" },
          "duration_ms": { "type": ["integer", "null"] }
        }
      },
      "HubWebhookEnvelope": {
        "type": "object",
        "description": "Common envelope for hub webhook deliveries. Signature/delivery rules: `X-Quiqqy-Signature` = HMAC-SHA256(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", "community_id", "data", "timestamp"],
        "properties": {
          "event_type": { "type": "string" },
          "event_id": { "type": "string", "description": "Unique event id — equals `X-Quiqqy-Delivery-Id`. Dedupe on it." },
          "community_id": { "type": "string" },
          "data": { "type": "object", "additionalProperties": true },
          "timestamp": { "type": "string", "format": "date-time" }
        }
      },
      "HubOrderCreatedEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/HubWebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "order.created" },
              "data": {
                "type": "object",
                "required": ["order"],
                "properties": {
                  "order": { "$ref": "#/components/schemas/HubOrder" },
                  "test": { "type": "boolean", "description": "Present and `true` on test deliveries — never create a real order from one." }
                }
              }
            }
          }
        ]
      },
      "HubOrderStatusChangedEnvelope": {
        "allOf": [
          { "$ref": "#/components/schemas/HubWebhookEnvelope" },
          {
            "type": "object",
            "properties": {
              "event_type": { "type": "string", "const": "order.status_changed" },
              "data": {
                "type": "object",
                "required": ["order", "previous_status"],
                "properties": {
                  "previous_status": { "$ref": "#/components/schemas/HubOrderStatus" },
                  "order": { "$ref": "#/components/schemas/HubOrder" }
                }
              }
            }
          }
        ]
      }
    }
  }
}
