{
  "openapi": "3.0.0",
  "paths": {
    "/customers": {
      "get": {
        "operationId": "listCustomers",
        "parameters": [
          {
            "name": "order",
            "required": false,
            "in": "query",
            "description": "Sort order (asc or desc)",
            "schema": {
              "default": "desc",
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "sort",
            "required": false,
            "in": "query",
            "description": "Field to sort by",
            "schema": {
              "example": "entryDate",
              "type": "string"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Page number (1-based)",
            "schema": {
              "minimum": 1,
              "default": 1,
              "type": "number"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "minimum": 1,
              "default": 10,
              "type": "number"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "description": "Search by name, email, mobile, or loyalty number",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "Filter by status (1 = active, 0 = inactive)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "email",
            "required": false,
            "in": "query",
            "description": "Filter by email",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mobile",
            "required": false,
            "in": "query",
            "description": "Filter by mobile",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "List customers",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:ONLINE_ORDER",
          "Partner:RESERVATION",
          "Partner:TABLE_QR_PAYMENT"
        ]
      },
      "post": {
        "operationId": "createCustomer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerCreateCustomerDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Create a customer",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:ONLINE_ORDER",
          "Partner:RESERVATION",
          "Partner:TABLE_QR_PAYMENT"
        ]
      }
    },
    "/customers/{id}": {
      "get": {
        "operationId": "getCustomer",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Get a customer",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:ONLINE_ORDER",
          "Partner:RESERVATION",
          "Partner:TABLE_QR_PAYMENT"
        ]
      },
      "patch": {
        "operationId": "updateCustomer",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "number"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PartnerUpdateCustomerDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Update a customer",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:ONLINE_ORDER",
          "Partner:RESERVATION",
          "Partner:TABLE_QR_PAYMENT"
        ]
      }
    },
    "/reservations": {
      "get": {
        "description": "Ordered by reservation date, then time (`order`, default `desc`); `sort` is not accepted. `fromDate`/`toDate` bound the reservation date inclusively; `search` is a contains-match on the customer's name, mobile and email and the booking's phone and email. Rows use the stored PascalCase column names (`PartySize`, `DrationMinutes`, `Status`, …) with `CustomerName` and `StoreName` joined in; `Phone`/`Email`/`Address` fall back to the customer record.",
        "operationId": "listReservations",
        "parameters": [
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Page number (1-based)",
            "schema": {
              "minimum": 1,
              "default": 1,
              "type": "number"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Rows per page. Values above 100 are clamped to 100.",
            "schema": {
              "minimum": 1,
              "maximum": 100,
              "default": 10,
              "type": "number"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "description": "Free-text search",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fromDate",
            "required": false,
            "in": "query",
            "description": "Start of the date window (YYYY-MM-DD)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "toDate",
            "required": false,
            "in": "query",
            "description": "End of the date window (YYYY-MM-DD)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "Reservation lifecycle status",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "confirmed",
                "seated",
                "completed",
                "cancelled"
              ]
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "description": "Filter by customer id",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "storeId",
            "required": false,
            "in": "query",
            "description": "Filter by store id",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "List reservations",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:RESERVATION"
        ]
      },
      "post": {
        "description": "Returns `{ data: { reservationId } }`. Date and time are the store wall clock. Always send `storeId`: only a single-store merchant falls back to its store. The store's booking rules apply — 400 `reservations.disabled`, `date_in_past`, `party_too_small`, `party_too_large`, `too_far_ahead`, `notice_too_short`, `customer_already_has_reservation`; 409 `day_full`, `slot_full`, `seats_full`, `busy` (all prefixed `reservations.`).",
        "operationId": "createReservation",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateReservationDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Create a reservation",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:RESERVATION"
        ]
      }
    },
    "/reservations/{id}": {
      "get": {
        "description": "One reservation row, as in the list plus `CreatedBy`/`UpdatedBy`. 404 `reservations.not_found` when the id does not exist in this merchant.",
        "operationId": "getReservation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Get a reservation",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:RESERVATION"
        ]
      },
      "patch": {
        "description": "Partial: send only the fields that change; `null` clears a text field. Status moves follow pending → confirmed|cancelled, confirmed → seated|completed|cancelled, seated → completed|cancelled; `completed` and `cancelled` are final (409 `reservations.invalid_status_transition`). Moving a booking that holds a table re-runs the store's booking rules. Returns `{ data: { reservationId } }`.",
        "operationId": "updateReservation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "number"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateReservationDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "Update a reservation",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:RESERVATION"
        ]
      }
    },
    "/stores": {
      "get": {
        "operationId": "listStores",
        "parameters": [
          {
            "name": "order",
            "required": false,
            "in": "query",
            "description": "Sort order (asc or desc)",
            "schema": {
              "default": "desc",
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "sort",
            "required": false,
            "in": "query",
            "description": "Field to sort by",
            "schema": {
              "example": "entryDate",
              "type": "string"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Page number (1-based)",
            "schema": {
              "minimum": 1,
              "default": 1,
              "type": "number"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Number of items per page",
            "schema": {
              "minimum": 1,
              "default": 10,
              "type": "number"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "description": "Search by name, code, or email",
            "schema": {
              "example": "Main Store",
              "type": "string"
            }
          },
          {
            "name": "isActive",
            "required": false,
            "in": "query",
            "description": "Filter by active status",
            "schema": {
              "example": true,
              "type": "boolean"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "description": "1 = active only, 0 = inactive only",
            "schema": {
              "example": 1,
              "type": "number",
              "enum": [
                0,
                1
              ]
            }
          },
          {
            "name": "archived",
            "required": false,
            "in": "query",
            "description": "Restrict to archived (true) or non-archived (false) stores",
            "schema": {
              "example": false,
              "type": "boolean"
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "description": "0 = stores only, 1 = warehouses only; omit for both",
            "schema": {
              "example": 0,
              "type": "number",
              "enum": [
                0,
                1
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing, malformed or expired app token"
          },
          "403": {
            "description": "Token lacks the required scope (partner.scope_denied), is used against the wrong environment (partner.env_mismatch), or comes from a non-allowlisted IP (partner.ip_denied)"
          },
          "429": {
            "description": "Per-app rate limit exceeded (partner.rate_limited)"
          }
        },
        "security": [
          {
            "appToken": []
          }
        ],
        "summary": "List stores",
        "tags": [
          "Partner",
          "Partner:FULL",
          "Partner:ONLINE_ORDER",
          "Partner:ADSR",
          "Partner:RESERVATION",
          "Partner:TABLE_QR_PAYMENT"
        ]
      }
    }
  },
  "info": {
    "title": "LithosPOS RESERVATION API",
    "description": "Table bookings for reservation platforms, booking widgets and concierge tools: read the stores, find or create the guest, then create reservations and move them through their lifecycle.\n\nAuthenticate with an app access token — `POST /v1/oauth/token` (authorization code or refresh token, after the merchant authorises the app) or `POST /v1/token` (client credentials, sandbox quickstart). The token selects the environment and the merchant.\n\nCall every path below against `https://api.lithospos.com/v1` — one gateway host for both sandbox and production. The `api_base_url` in the token response is authoritative: prefer it over this document if the two differ.",
    "version": "1.0",
    "contact": {}
  },
  "tags": [],
  "servers": [
    {
      "url": "https://api.lithospos.com/v1",
      "description": "Global API gateway — same host for sandbox and production; the token response api_base_url is authoritative"
    }
  ],
  "components": {
    "securitySchemes": {
      "appToken": {
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "type": "http"
      }
    },
    "schemas": {
      "PartnerCreateCustomerDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Customer name",
            "maxLength": 150
          },
          "mobile": {
            "type": "string",
            "description": "Mobile number",
            "maxLength": 40
          },
          "email": {
            "type": "string",
            "description": "Email address",
            "maxLength": 65
          },
          "loyaltyNo": {
            "type": "string",
            "description": "Loyalty number",
            "maxLength": 20
          },
          "trn": {
            "type": "string",
            "description": "Tax Registration Number",
            "maxLength": 20
          },
          "countryId": {
            "type": "number",
            "description": "Country ID",
            "default": 0
          },
          "stateId": {
            "type": "number",
            "description": "State ID",
            "default": 0
          },
          "state": {
            "type": "string",
            "description": "State name",
            "maxLength": 45
          },
          "address": {
            "type": "string",
            "description": "Address"
          },
          "status": {
            "type": "number",
            "description": "Status (1 = active, 0 = inactive)",
            "default": 1
          },
          "creditCust": {
            "type": "number",
            "description": "Credit customer flag",
            "default": 0
          },
          "creditLimit": {
            "type": "number",
            "description": "Credit limit"
          },
          "opening": {
            "type": "number",
            "description": "Opening balance"
          },
          "loyaltyPoint": {
            "type": "number",
            "description": "Loyalty points",
            "default": 0
          },
          "birthDate": {
            "type": "string",
            "description": "Birth date (YYYY-MM-DD)"
          },
          "anniversary": {
            "type": "string",
            "description": "Anniversary date (YYYY-MM-DD)"
          },
          "firebaseUserId": {
            "type": "string",
            "description": "Firebase user ID",
            "maxLength": 200
          },
          "latitude": {
            "type": "number",
            "description": "Latitude"
          },
          "longitude": {
            "type": "number",
            "description": "Longitude"
          },
          "landmark": {
            "type": "string",
            "description": "Landmark"
          },
          "ZIP": {
            "type": "string",
            "description": "ZIP code",
            "maxLength": 45
          },
          "StoreId": {
            "type": "number",
            "description": "Store ID",
            "default": 0
          },
          "TaxExempt": {
            "type": "number",
            "description": "Tax exempt flag",
            "default": 0
          }
        },
        "required": [
          "name"
        ]
      },
      "PartnerUpdateCustomerDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Customer name",
            "maxLength": 150
          },
          "mobile": {
            "type": "string",
            "description": "Mobile number",
            "maxLength": 40
          },
          "email": {
            "type": "string",
            "description": "Email address",
            "maxLength": 65
          },
          "loyaltyNo": {
            "type": "string",
            "description": "Loyalty number",
            "maxLength": 20
          },
          "trn": {
            "type": "string",
            "description": "Tax Registration Number",
            "maxLength": 20
          },
          "countryId": {
            "type": "number",
            "description": "Country ID",
            "default": 0
          },
          "stateId": {
            "type": "number",
            "description": "State ID",
            "default": 0
          },
          "state": {
            "type": "string",
            "description": "State name",
            "maxLength": 45
          },
          "address": {
            "type": "string",
            "description": "Address"
          },
          "status": {
            "type": "number",
            "description": "Status (1 = active, 0 = inactive)",
            "default": 1
          },
          "creditCust": {
            "type": "number",
            "description": "Credit customer flag",
            "default": 0
          },
          "creditLimit": {
            "type": "number",
            "description": "Credit limit"
          },
          "opening": {
            "type": "number",
            "description": "Opening balance"
          },
          "loyaltyPoint": {
            "type": "number",
            "description": "Loyalty points",
            "default": 0
          },
          "birthDate": {
            "type": "string",
            "description": "Birth date (YYYY-MM-DD)"
          },
          "anniversary": {
            "type": "string",
            "description": "Anniversary date (YYYY-MM-DD)"
          },
          "firebaseUserId": {
            "type": "string",
            "description": "Firebase user ID",
            "maxLength": 200
          },
          "latitude": {
            "type": "number",
            "description": "Latitude"
          },
          "longitude": {
            "type": "number",
            "description": "Longitude"
          },
          "landmark": {
            "type": "string",
            "description": "Landmark"
          },
          "ZIP": {
            "type": "string",
            "description": "ZIP code",
            "maxLength": 45
          },
          "StoreId": {
            "type": "number",
            "description": "Store ID",
            "default": 0
          },
          "TaxExempt": {
            "type": "number",
            "description": "Tax exempt flag",
            "default": 0
          }
        }
      },
      "CreateReservationDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "number",
            "description": "Customer ID"
          },
          "reservationDate": {
            "type": "string",
            "description": "Reservation date (YYYY-MM-DD)"
          },
          "reservationTime": {
            "type": "string",
            "description": "Reservation time (HH:mm or HH:mm:ss, 24h)"
          },
          "partySize": {
            "type": "number",
            "description": "Party size — number of guests"
          },
          "durationMinutes": {
            "type": "number",
            "description": "Duration in minutes (default: the store's reservation setting, else 120)"
          },
          "status": {
            "type": "string",
            "description": "Booking status. Omitted on create → `confirmed` when the store auto-accepts, else `pending`. On update the change must follow the lifecycle (pending → confirmed|cancelled, confirmed → seated|completed|cancelled, seated → completed|cancelled).",
            "enum": [
              "pending",
              "confirmed",
              "seated",
              "completed",
              "cancelled"
            ]
          },
          "occasion": {
            "type": "string",
            "description": "Occasion label (free text — \"Birthday\", \"Anniversary\", \"Date Night\"…)."
          },
          "storeId": {
            "type": "number",
            "description": "Store the reservation belongs to"
          },
          "note": {
            "type": "string",
            "description": "Free-text note for front-of-house"
          },
          "phone": {
            "type": "string",
            "description": "Guest phone (denormalized snapshot)"
          },
          "email": {
            "type": "string",
            "description": "Guest email (denormalized snapshot)"
          },
          "address": {
            "type": "string",
            "description": "Guest address"
          },
          "customerPreferences": {
            "type": "string",
            "description": "Guest preferences (allergies, seating, …)"
          },
          "customerPhotoUrl": {
            "type": "string",
            "description": "Guest photo URL (for VIPs / regulars)"
          },
          "preOrderedFoods": {
            "type": "array",
            "description": "Pre-ordered food lines — stored as JSON in the DB. Shape: [{ name, quantity, price, notes }]",
            "items": {
              "type": "object"
            }
          },
          "extra": {
            "type": "object",
            "description": "Raw pass-through JSON for extensions"
          }
        },
        "required": [
          "customerId",
          "reservationDate",
          "reservationTime",
          "partySize"
        ]
      },
      "UpdateReservationDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "number",
            "description": "Customer ID"
          },
          "reservationDate": {
            "type": "string",
            "description": "Reservation date (YYYY-MM-DD)"
          },
          "reservationTime": {
            "type": "string",
            "description": "Reservation time (HH:mm or HH:mm:ss, 24h)"
          },
          "partySize": {
            "type": "number",
            "description": "Party size — number of guests"
          },
          "durationMinutes": {
            "type": "number",
            "description": "Duration in minutes (default: the store's reservation setting, else 120)"
          },
          "status": {
            "type": "string",
            "description": "Booking status. Omitted on create → `confirmed` when the store auto-accepts, else `pending`. On update the change must follow the lifecycle (pending → confirmed|cancelled, confirmed → seated|completed|cancelled, seated → completed|cancelled).",
            "enum": [
              "pending",
              "confirmed",
              "seated",
              "completed",
              "cancelled"
            ]
          },
          "occasion": {
            "type": "string",
            "description": "Occasion label (free text — \"Birthday\", \"Anniversary\", \"Date Night\"…)."
          },
          "storeId": {
            "type": "number",
            "description": "Store the reservation belongs to"
          },
          "note": {
            "type": "string",
            "description": "Free-text note for front-of-house"
          },
          "phone": {
            "type": "string",
            "description": "Guest phone (denormalized snapshot)"
          },
          "email": {
            "type": "string",
            "description": "Guest email (denormalized snapshot)"
          },
          "address": {
            "type": "string",
            "description": "Guest address"
          },
          "customerPreferences": {
            "type": "string",
            "description": "Guest preferences (allergies, seating, …)"
          },
          "customerPhotoUrl": {
            "type": "string",
            "description": "Guest photo URL (for VIPs / regulars)"
          },
          "preOrderedFoods": {
            "type": "array",
            "description": "Pre-ordered food lines — stored as JSON in the DB. Shape: [{ name, quantity, price, notes }]",
            "items": {
              "type": "object"
            }
          },
          "extra": {
            "type": "object",
            "description": "Raw pass-through JSON for extensions"
          }
        }
      }
    }
  }
}
