{
  "openapi": "3.0.0",
  "paths": {
    "/v1/catalog/products": {
      "get": {
        "description": "Paginated, filterable list of the external-safe product catalog for the authenticated tenant and allowed stores. Required scope: `catalog.products.read`.",
        "operationId": "ExternalCatalogController_listProducts",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "description": "Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "1-indexed page number.",
            "schema": {
              "minimum": 1,
              "default": 1,
              "example": 1,
              "type": "number"
            }
          },
          {
            "name": "pageSize",
            "required": false,
            "in": "query",
            "description": "Rows per page.",
            "schema": {
              "minimum": 1,
              "maximum": 100,
              "default": 50,
              "example": 50,
              "type": "number"
            }
          },
          {
            "name": "storeId",
            "required": false,
            "in": "query",
            "description": "Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.",
            "schema": {
              "example": "clx1store0000001",
              "type": "string"
            }
          },
          {
            "name": "categoryId",
            "required": false,
            "in": "query",
            "schema": {
              "example": "clx1category000001",
              "type": "string"
            }
          },
          {
            "name": "q",
            "required": false,
            "in": "query",
            "description": "Free-text search across name, SKU, barcode and brand (case-insensitive).",
            "schema": {
              "example": "bread",
              "type": "string"
            }
          },
          {
            "name": "barcode",
            "required": false,
            "in": "query",
            "schema": {
              "example": "5000000000001",
              "type": "string"
            }
          },
          {
            "name": "sku",
            "required": false,
            "in": "query",
            "schema": {
              "example": "SKU-ALPHA-BREAD",
              "type": "string"
            }
          },
          {
            "name": "updatedSince",
            "required": false,
            "in": "query",
            "description": "Only return products updated at or after this instant.",
            "schema": {
              "format": "date-time",
              "example": "2026-08-01T00:00:00.000Z",
              "type": "string"
            }
          },
          {
            "name": "activeOnly",
            "required": false,
            "in": "query",
            "description": "When true (default), only active products are returned.",
            "schema": {
              "default": true,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogProductListResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "validation_failed - the request query/params failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large - request exceeded the configured body size limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 42
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "500": {
            "description": "internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "IntegrationApiKey": []
          }
        ],
        "summary": "List catalog products",
        "tags": [
          "Catalog"
        ],
        "x-epos-scopes": [
          "catalog.products.read"
        ]
      }
    },
    "/v1/catalog/products/{id}": {
      "get": {
        "description": "Fetch a single product by ID. Returns 404 if the product does not exist, or exists outside the authenticated tenant/allowed stores. Required scope: `catalog.products.read`.",
        "operationId": "ExternalCatalogController_getProduct",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "description": "Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Product ID.",
            "schema": {
              "example": "clx1product000001",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogProductDto"
                }
              }
            }
          },
          "400": {
            "description": "validation_failed - the request query/params failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large - request exceeded the configured body size limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 42
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "500": {
            "description": "internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "IntegrationApiKey": []
          }
        ],
        "summary": "Get a catalog product",
        "tags": [
          "Catalog"
        ],
        "x-epos-scopes": [
          "catalog.products.read"
        ]
      }
    },
    "/v1/catalog/categories": {
      "get": {
        "description": "Filterable list of external-safe product categories for the authenticated tenant and allowed stores. Not paginated. Required scope: `catalog.categories.read`.",
        "operationId": "ExternalCatalogController_listCategories",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "description": "Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "storeId",
            "required": false,
            "in": "query",
            "description": "Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.",
            "schema": {
              "example": "clx1store0000001",
              "type": "string"
            }
          },
          {
            "name": "parentCategoryId",
            "required": false,
            "in": "query",
            "schema": {
              "example": "clx1category000001",
              "type": "string"
            }
          },
          {
            "name": "activeOnly",
            "required": false,
            "in": "query",
            "description": "When true (default), only active categories are returned. Archived categories are always excluded.",
            "schema": {
              "default": true,
              "type": "boolean"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "Sort order. Defaults to sortOrder, then name.",
            "schema": {
              "example": "sortOrder",
              "type": "string",
              "enum": [
                "name",
                "sortOrder"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CatalogCategoryDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "validation_failed - the request query/params failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large - request exceeded the configured body size limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 42
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "500": {
            "description": "internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "IntegrationApiKey": []
          }
        ],
        "summary": "List catalog categories",
        "tags": [
          "Catalog"
        ],
        "x-epos-scopes": [
          "catalog.categories.read"
        ]
      }
    },
    "/v1/catalog/promotions": {
      "get": {
        "description": "Filterable list of customer-facing, external-safe promotions for the authenticated tenant and allowed stores. Not paginated. Coupon, staff-discount and manual-discount promotion types, POS-only system-generated presets, and DRAFT/ARCHIVED promotions are never returned by this endpoint, regardless of filters. Required scope: `catalog.promotions.read`.",
        "operationId": "ExternalCatalogController_listPromotions",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "description": "Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "storeId",
            "required": false,
            "in": "query",
            "description": "Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.",
            "schema": {
              "example": "clx1store0000001",
              "type": "string"
            }
          },
          {
            "name": "activeAt",
            "required": false,
            "in": "query",
            "description": "Return promotions active at this instant (status ACTIVE and inside [startsAt, endsAt]). Forces the same status+window restriction as activeOnly=true regardless of the activeOnly value passed alongside it.",
            "schema": {
              "format": "date-time",
              "example": "2026-08-27T18:00:00.000Z",
              "type": "string"
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "description": "Exact match on promotion type. A type outside the customer-facing allow-list simply returns an empty array.",
            "schema": {
              "example": "MEAL_DEAL",
              "type": "string"
            }
          },
          {
            "name": "activeOnly",
            "required": false,
            "in": "query",
            "description": "When true (default), only ACTIVE promotions currently inside their active window are returned. When false, PAUSED promotions are also included (still never DRAFT/ARCHIVED).",
            "schema": {
              "default": true,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CatalogPromotionDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "validation_failed - the request query/params failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large - request exceeded the configured body size limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 42
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "500": {
            "description": "internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "IntegrationApiKey": []
          }
        ],
        "summary": "List catalog promotions",
        "tags": [
          "Catalog"
        ],
        "x-epos-scopes": [
          "catalog.promotions.read"
        ]
      }
    },
    "/v1/inventory/stock": {
      "get": {
        "description": "Paginated, filterable list of exact stock levels for the authenticated tenant and allowed stores. Returns stock visibility only - no cost/margin/supplier/movement-history data. Required scope: `inventory.stock.read`.",
        "operationId": "ExternalInventoryController_listStock",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "description": "Optional client-supplied request ID (`^[A-Za-z0-9_.:-]{1,80}$`). Echoed back on the `X-Request-Id` response header and inside any error body's `requestId` field. If omitted or invalid, the server generates one (`req_...`).",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "1-indexed page number.",
            "schema": {
              "minimum": 1,
              "default": 1,
              "example": 1,
              "type": "number"
            }
          },
          {
            "name": "pageSize",
            "required": false,
            "in": "query",
            "description": "Rows per page.",
            "schema": {
              "minimum": 1,
              "maximum": 100,
              "default": 50,
              "example": 50,
              "type": "number"
            }
          },
          {
            "name": "storeId",
            "required": false,
            "in": "query",
            "description": "Restrict to one store. Must be a store this IntegrationClient is allowed to access; otherwise 404.",
            "schema": {
              "example": "clx1store0000001",
              "type": "string"
            }
          },
          {
            "name": "productId",
            "required": false,
            "in": "query",
            "schema": {
              "example": "clx1product000001",
              "type": "string"
            }
          },
          {
            "name": "sku",
            "required": false,
            "in": "query",
            "schema": {
              "example": "SKU-ALPHA-BREAD",
              "type": "string"
            }
          },
          {
            "name": "barcode",
            "required": false,
            "in": "query",
            "schema": {
              "example": "5000000000001",
              "type": "string"
            }
          },
          {
            "name": "categoryId",
            "required": false,
            "in": "query",
            "schema": {
              "example": "clx1category000001",
              "type": "string"
            }
          },
          {
            "name": "updatedSince",
            "required": false,
            "in": "query",
            "description": "Only return products updated at or after this instant.",
            "schema": {
              "format": "date-time",
              "example": "2026-08-01T00:00:00.000Z",
              "type": "string"
            }
          },
          {
            "name": "lowStockOnly",
            "required": false,
            "in": "query",
            "description": "When true, only products with availability=low_stock are returned.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "activeOnly",
            "required": false,
            "in": "query",
            "description": "When true (default), only active products are returned.",
            "schema": {
              "default": true,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InventoryStockListResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "validation_failed - the request query/params failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "401": {
            "description": "unauthorized - missing, malformed, expired or revoked API key, or the owning IntegrationClient is disabled/revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "403": {
            "description": "scope_denied - the API key is valid but the IntegrationClient does not hold the scope this route requires.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "404": {
            "description": "not_found - the resource does not exist, or exists but is outside the IntegrationClient's tenant/allowed stores. Both cases return the same generic 404 deliberately, to avoid confirming a resource exists to a caller not allowed to see it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "413": {
            "description": "payload_too_large - request exceeded the configured body size limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited - too many requests. See the `Retry-After` response header. Current numeric limits are an operational policy, not a contractual guarantee, and may change - implement retry/backoff rather than hardcoding a specific limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "example": 42
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          },
          "500": {
            "description": "internal_error - unexpected server error. The response body is deliberately generic; use `requestId` when reporting the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExternalApiErrorDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "IntegrationApiKey": []
          }
        ],
        "summary": "List inventory stock",
        "tags": [
          "Inventory"
        ],
        "x-epos-scopes": [
          "inventory.stock.read"
        ]
      }
    }
  },
  "info": {
    "title": "EPOS Developer API",
    "description": "Public/partner API for approved EPOS integrations. Every route lives under `/v1` and is versioned, redacted and rate-limited independently of EPOS's internal (unversioned) API, which is out of scope for this document and never appears in it. Access requires an Integration API key issued to an approved IntegrationClient, scoped to specific capabilities and optionally restricted to specific stores. This document is a DEV review artifact and is not yet published to a public developer docs site.",
    "version": "1.0.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "Catalog",
      "description": "Products, categories and customer-facing promotions."
    },
    {
      "name": "Inventory",
      "description": "Exact inventory stock levels."
    }
  ],
  "servers": [
    {
      "url": "https://dev-api.theprioryshop.co.uk",
      "description": "Development"
    },
    {
      "url": "https://api.theprioryshop.co.uk",
      "description": "Production (this server existing does not mean the public API has launched - see the Phase 2 review workflow)"
    }
  ],
  "components": {
    "securitySchemes": {
      "IntegrationApiKey": {
        "scheme": "bearer",
        "bearerFormat": "EPOS integration API key issued to an IntegrationClient",
        "type": "http",
        "description": "EPOS integration API key issued to an IntegrationClient via Back Office Developer Tools > Integrations. Not a JWT, not OAuth2 - a static API key. Format: `Authorization: Bearer epos_live_sk_...` (production) or `Bearer epos_dev_sk_...` (development). Staff/Back Office JWT sessions are never valid for `/v1` - only Integration API keys are."
      }
    },
    "schemas": {
      "ExternalApiErrorBodyDto": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code. See the Error codes table in the API reference.",
            "example": "validation_failed",
            "enum": [
              "validation_failed",
              "unauthorized",
              "scope_denied",
              "not_found",
              "payload_too_large",
              "rate_limited",
              "internal_error"
            ]
          },
          "message": {
            "type": "string",
            "description": "Human-readable message. Safe to show to a developer, not localized, not guaranteed to be stable text - match on `code`, not this string.",
            "example": "Request validation failed."
          },
          "details": {
            "type": "object",
            "description": "Additional machine-readable detail, shape depends on `code` (e.g. validation errors, required scopes). Empty object when there is nothing more to add.",
            "additionalProperties": true,
            "example": {}
          },
          "requestId": {
            "type": "string",
            "description": "Request ID for this response - same value as the `X-Request-Id` response header. Include this when reporting a problem.",
            "example": "req_AbCdEfGh12345678"
          }
        },
        "required": [
          "code",
          "message",
          "details",
          "requestId"
        ]
      },
      "ExternalApiErrorDto": {
        "type": "object",
        "properties": {
          "error": {
            "$ref": "#/components/schemas/ExternalApiErrorBodyDto"
          }
        },
        "required": [
          "error"
        ]
      },
      "CatalogProductVatRateDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "clx1vatrate000001"
          },
          "code": {
            "type": "string",
            "description": "Short VAT rate code.",
            "example": "STD"
          },
          "name": {
            "type": "string",
            "example": "Standard Rate"
          },
          "percentage": {
            "type": "string",
            "description": "VAT percentage, serialized as a decimal string.",
            "example": "20.00"
          }
        },
        "required": [
          "id",
          "code",
          "name",
          "percentage"
        ]
      },
      "CatalogProductDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "clx1product000001"
          },
          "storeId": {
            "type": "string",
            "example": "clx1store0000001"
          },
          "categoryId": {
            "type": "object",
            "nullable": true,
            "example": "clx1category000001"
          },
          "name": {
            "type": "string",
            "example": "Alpha Bread 800g"
          },
          "sku": {
            "type": "string",
            "example": "SKU-ALPHA-BREAD"
          },
          "barcode": {
            "type": "object",
            "nullable": true,
            "example": "5000000000001"
          },
          "brand": {
            "type": "object",
            "nullable": true,
            "example": "Priory"
          },
          "size": {
            "type": "object",
            "nullable": true,
            "example": "800g"
          },
          "unitType": {
            "type": "string",
            "description": "Unit the product is sold/measured in.",
            "enum": [
              "item",
              "kg",
              "g",
              "litre",
              "ml"
            ],
            "example": "item"
          },
          "sellingPriceGross": {
            "type": "string",
            "description": "Selling price including VAT, as a decimal string.",
            "example": "1.25"
          },
          "sellingPriceNet": {
            "type": "string",
            "description": "Selling price excluding VAT, as a decimal string.",
            "example": "1.04"
          },
          "taxCategory": {
            "type": "string",
            "enum": [
              "STANDARD",
              "ZERO_RATED",
              "REDUCED",
              "EXEMPT",
              "OUT_OF_SCOPE",
              "UNCLASSIFIED"
            ],
            "example": "STANDARD"
          },
          "vatRate": {
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/CatalogProductVatRateDto"
              }
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "variablePrice": {
            "type": "boolean",
            "description": "Whether this product is sold at a variable/scale-entered price (e.g. deli, bakery by weight)."
          },
          "ageRestricted": {
            "type": "boolean"
          },
          "alcoholRestricted": {
            "type": "boolean"
          },
          "tobaccoRestricted": {
            "type": "boolean"
          },
          "vapeRestricted": {
            "type": "boolean"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-27T17:51:41.976Z"
          }
        },
        "required": [
          "id",
          "storeId",
          "categoryId",
          "name",
          "sku",
          "barcode",
          "brand",
          "size",
          "unitType",
          "sellingPriceGross",
          "sellingPriceNet",
          "taxCategory",
          "vatRate",
          "isActive",
          "variablePrice",
          "ageRestricted",
          "alcoholRestricted",
          "tobaccoRestricted",
          "vapeRestricted",
          "updatedAt"
        ]
      },
      "CatalogProductListResponseDto": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogProductDto"
            }
          },
          "page": {
            "type": "number",
            "description": "1-indexed current page.",
            "example": 1
          },
          "pageSize": {
            "type": "number",
            "description": "Rows per page (max 100).",
            "example": 50
          },
          "total": {
            "type": "number",
            "description": "Total rows matching the filters, independent of pagination.",
            "example": 479
          },
          "totalPages": {
            "type": "number",
            "description": "ceil(total / pageSize), minimum 1.",
            "example": 10
          }
        },
        "required": [
          "rows",
          "page",
          "pageSize",
          "total",
          "totalPages"
        ]
      },
      "CatalogCategoryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "clx1category000001"
          },
          "storeId": {
            "type": "string",
            "example": "clx1store0000001"
          },
          "parentCategoryId": {
            "type": "object",
            "nullable": true,
            "example": null
          },
          "name": {
            "type": "string",
            "example": "Bakery"
          },
          "code": {
            "type": "object",
            "nullable": true,
            "example": "BAKE"
          },
          "description": {
            "type": "object",
            "nullable": true,
            "example": null
          },
          "sortOrder": {
            "type": "number",
            "example": 1
          },
          "isActive": {
            "type": "boolean"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-27T17:51:41.976Z"
          }
        },
        "required": [
          "id",
          "storeId",
          "parentCategoryId",
          "name",
          "code",
          "description",
          "sortOrder",
          "isActive",
          "updatedAt"
        ]
      },
      "CatalogPromotionDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "clx1promo00000001"
          },
          "storeId": {
            "type": "string",
            "example": "clx1store0000001"
          },
          "name": {
            "type": "string",
            "example": "Meal Deal: Main + Snack + Drink = £3.50"
          },
          "description": {
            "type": "object",
            "nullable": true,
            "example": null
          },
          "type": {
            "type": "string",
            "description": "Customer-facing promotion type. Coupon, staff-discount and manual-discount promotion types are never returned by this API - see the API reference's Promotions section.",
            "enum": [
              "MEAL_DEAL",
              "MULTIBUY_SAME_PRODUCT",
              "ANY_N_FIXED_PRICE",
              "MIX_MATCH_FIXED_PRICE",
              "MULTIBUY_FIXED_PRICE",
              "PRICE_CUT_FIXED_PRICE",
              "PRICE_CUT_PERCENT",
              "PRICE_CUT_FIXED",
              "TIMED_REDUCTION_PERCENT",
              "TIMED_REDUCTION_FIXED",
              "LOYALTY_PRICE"
            ],
            "example": "MEAL_DEAL"
          },
          "startsAt": {
            "type": "object",
            "format": "date-time",
            "nullable": true,
            "example": null
          },
          "endsAt": {
            "type": "object",
            "format": "date-time",
            "nullable": true,
            "example": null
          },
          "isActive": {
            "type": "boolean",
            "description": "True when status is ACTIVE and the current time is inside [startsAt, endsAt]. Computed independently of the activeOnly/activeAt query filters."
          },
          "eligibilitySummary": {
            "type": "string",
            "description": "Plain-language description of who/what qualifies. Never the raw internal conditions JSON.",
            "example": "Buy one qualifying item from each of the Main, Snack and Drink groups"
          },
          "offerSummary": {
            "type": "string",
            "description": "Plain-language description of the discount. Never the raw internal actions JSON.",
            "example": "Fixed price £3.50"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-27T17:51:41.976Z"
          }
        },
        "required": [
          "id",
          "storeId",
          "name",
          "description",
          "type",
          "startsAt",
          "endsAt",
          "isActive",
          "eligibilitySummary",
          "offerSummary",
          "updatedAt"
        ]
      },
      "InventoryStockItemDto": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "example": "clx1product000001"
          },
          "storeId": {
            "type": "string",
            "example": "clx1store0000001"
          },
          "sku": {
            "type": "string",
            "example": "SKU-ALPHA-BREAD"
          },
          "barcode": {
            "type": "object",
            "nullable": true,
            "example": "5000000000001"
          },
          "name": {
            "type": "string",
            "example": "Alpha Bread 800g"
          },
          "trackStock": {
            "type": "boolean",
            "description": "Whether this product's stock level is tracked at all. When false, availability is always not_tracked."
          },
          "stockQuantity": {
            "type": "string",
            "description": "Exact current stock quantity, as a decimal string.",
            "example": "6.000"
          },
          "minimumStockLevel": {
            "type": "string",
            "description": "Informational only - mirrored from reorderLevel on save, not independently used for the availability calculation.",
            "example": "0.000"
          },
          "reorderLevel": {
            "type": "string",
            "description": "The threshold actually used to compute `low_stock`.",
            "example": "5.000"
          },
          "availability": {
            "type": "string",
            "description": "not_tracked when trackStock is false. Otherwise: out_of_stock (<=0), low_stock (>0 and <= reorderLevel), in_stock (> reorderLevel).",
            "enum": [
              "in_stock",
              "low_stock",
              "out_of_stock",
              "not_tracked"
            ],
            "example": "in_stock"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-08-27T17:51:41.976Z"
          }
        },
        "required": [
          "productId",
          "storeId",
          "sku",
          "barcode",
          "name",
          "trackStock",
          "stockQuantity",
          "minimumStockLevel",
          "reorderLevel",
          "availability",
          "updatedAt"
        ]
      },
      "InventoryStockListResponseDto": {
        "type": "object",
        "properties": {
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InventoryStockItemDto"
            }
          },
          "page": {
            "type": "number",
            "description": "1-indexed current page.",
            "example": 1
          },
          "pageSize": {
            "type": "number",
            "description": "Rows per page (max 100).",
            "example": 50
          },
          "total": {
            "type": "number",
            "description": "Total rows matching the filters, independent of pagination.",
            "example": 479
          },
          "totalPages": {
            "type": "number",
            "description": "ceil(total / pageSize), minimum 1.",
            "example": 10
          }
        },
        "required": [
          "rows",
          "page",
          "pageSize",
          "total",
          "totalPages"
        ]
      }
    }
  }
}
