{
  "openapi": "3.0.4",
  "info": {
    "title": "M3 external API",
    "description": "The M3 External API provides a comprehensive set of endpoints for accessing key salon data, including:\n\n* **Bookings**\n\n* **Customers**\n\n* **Products**\n\n* **Resources (Staff)**\n\n* **Sales**\n\n* **Salons**\n\n* **Services**\n\nThis API is specifically designed for integration with salon management systems and third-party platforms. It enables real-time access to operational data, supports automated scheduling and customer engagement, and facilitates financial tracking and reporting.\n\nBy integrating with the M3 API, external systems can:\n\n* **Synchronize booking and customer information**\n\n* **Retrieve service and product catalogs**\n\n* **Access staff availability and performance data**\n\n* **Analyze sales transactions and trends**\n\nThe API also supports **webhooks** for real-time event notifications to external systems.\n\nIt is optimized for performance, security, and scalability, making it suitable for both small salons and large enterprise platforms.",
    "version": "v1"
  },
  "paths": {
    "/api/v1/bookings": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Get bookings",
        "description": "The GetBooking endpoint retrieves all bookings associated with a salon within a specified date range.\n            \nIf no date range is provided, the endpoint defaults to returning bookings from the past 30 days.",
        "parameters": [
          {
            "name": "FilterDateType",
            "in": "query",
            "description": "Specifies which date field to use when filtering bookings: scheduled start date, creation date, or both.",
            "schema": {
              "enum": [
                0,
                1,
                2
              ],
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "StartDate",
            "in": "query",
            "description": "Start date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "End date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BookingViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BookingViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BookingViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Get customers",
        "description": "The GetCustomers endpoint retrieves all customers associated with a salon. It returns detailed customer information, including contact details, preferences, categories, and address data.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomerViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomerViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CustomerViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/products": {
      "get": {
        "tags": [
          "Products"
        ],
        "summary": "Get products",
        "description": "The GetProducts endpoint retrieves all products associated with a salon. It returns detailed product information, including product ID, product name, supplier ID, and supplier name.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProductViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProductViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProductViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/products/inventory": {
      "post": {
        "tags": [
          "Products"
        ],
        "summary": "Update inventory",
        "description": "The update inventory endpoint updates quantity of a product in a specific location based on the provided stock event type (e.g., sales).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PostInventoryInputModel"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PostInventoryInputModel"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PostInventoryInputModel"
              }
            }
          }
        },
        "responses": {
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/resources": {
      "get": {
        "tags": [
          "Resources"
        ],
        "summary": "Get resources",
        "description": "The GetResources endpoint retrieves all resources (staff) associated with a salon within a specified date range, defaulting to the last 30 days if no range is provided.\n            \nIt returns detailed resource information, including availability, occupancy, nicknames, online booking status, and pricing details.",
        "parameters": [
          {
            "name": "StartDate",
            "in": "query",
            "description": "Start date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "End date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ResourceViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ResourceViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ResourceViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales": {
      "get": {
        "tags": [
          "Sales"
        ],
        "summary": "Get sales",
        "description": "The GetSales endpoint retrieves all sales transactions associated with a salon within a specified date range, defaulting to the last 30 days if no range is provided.\n            \nIt returns detailed sales information, including transaction ID, product or service sold, quantity, price, customer, and date of sale.",
        "parameters": [
          {
            "name": "StartDate",
            "in": "query",
            "description": "Start date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "EndDate",
            "in": "query",
            "description": "End date to filter bookings.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SaleViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SaleViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SaleViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/salons": {
      "get": {
        "tags": [
          "Salons"
        ],
        "summary": "Get salons (locations)",
        "description": "The GetSalons endpoint retrieves all locations (salons) associated with a merchant.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SalonViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SalonViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SalonViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/services": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "Get services",
        "description": "The GetServices endpoint retrieves all services associated with a salon. It returns detailed service information, including service ID, add-on status, and service name, grouped and ordered by service name.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceViewModel"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceViewModel"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceViewModel"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "string"
                }
              },
              "text/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "BookingViewModel": {
        "type": "object",
        "properties": {
          "salonId": {
            "type": "string",
            "description": "Unique identifier of the salon where the booking was made.",
            "format": "uuid"
          },
          "salonName": {
            "type": "string",
            "description": "Name of the salon.",
            "nullable": true
          },
          "customerId": {
            "type": "string",
            "description": "Unique identifier of the customer who made the booking.",
            "format": "uuid",
            "nullable": true
          },
          "customerName": {
            "type": "string",
            "description": "Full name of the customer.",
            "nullable": true
          },
          "resourceId": {
            "type": "string",
            "description": "Unique identifier of the resource (e.g. staff member) assigned to the booking.",
            "format": "uuid"
          },
          "resourceName": {
            "type": "string",
            "description": "Full name of the assigned resource.",
            "nullable": true
          },
          "resourceNickName": {
            "type": "string",
            "description": "Optional nickname of the assigned resource.",
            "nullable": true
          },
          "bookingId": {
            "type": "string",
            "description": "Unique identifier of the booking.",
            "format": "uuid"
          },
          "bookingGroupId": {
            "type": "string",
            "description": "Identifier used to group related bookings (e.g. recurring or linked appointments).",
            "format": "uuid"
          },
          "created": {
            "type": "string",
            "description": "Date and time when the booking was created (in UTC).",
            "format": "date-time"
          },
          "serviceId": {
            "type": "string",
            "description": "Unique identifier of the booked service.",
            "format": "uuid"
          },
          "serviceName": {
            "type": "string",
            "description": "Name of the booked service.",
            "nullable": true
          },
          "onlineBooking": {
            "type": "boolean",
            "description": "Indicates whether the booking was made online."
          },
          "startDate": {
            "type": "string",
            "description": "Scheduled start date and time of the booking (in UTC).",
            "format": "date-time"
          },
          "startDateWithOffset": {
            "type": "string",
            "description": "Scheduled start date and time with timezone offset.",
            "format": "date-time"
          },
          "endDate": {
            "type": "string",
            "description": "Scheduled end date and time of the booking (in UTC).",
            "format": "date-time"
          },
          "endDateWithOffset": {
            "type": "string",
            "description": "Scheduled end date and time with timezone offset.",
            "format": "date-time"
          },
          "bookedPrice": {
            "type": "number",
            "description": "Total price of the booking at the time it was made.",
            "format": "double"
          },
          "cancelled": {
            "type": "boolean",
            "description": "Indicates whether the booking was cancelled."
          },
          "dropIn": {
            "type": "boolean",
            "description": "Indicates whether the booking was made as a drop-in (walk-in) appointment."
          },
          "rebooked": {
            "type": "boolean",
            "description": "Indicates whether the booking is a rebooking of a previous appointment."
          },
          "newBookingMade": {
            "type": "boolean",
            "description": "Indicates if this booking was the source of a re-booking. When true, a subsequent booking exists with its Rebooked"
          },
          "noShow": {
            "type": "boolean",
            "description": "Indicates whether the customer was marked as a no-show for the appointment."
          },
          "isAddon": {
            "type": "boolean",
            "description": "Indicates whether the booking is an add-on service to another booking."
          }
        },
        "additionalProperties": false
      },
      "CustomerViewModel": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "Unique identifier of the customer.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Full name of the customer.",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Email address of the customer.",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "description": "Phone number of the customer.",
            "nullable": true
          },
          "gender": {
            "enum": [
              0,
              1,
              2,
              3
            ],
            "type": "integer",
            "description": "Gender of the customer. (0 = Unknown, 1 = Female, 2 = Male, 3 = Other)",
            "format": "int32"
          },
          "allowEmail": {
            "type": "boolean",
            "description": "Indicates whether the customer has consented to receive communication via email."
          },
          "allowSms": {
            "type": "boolean",
            "description": "Indicates whether the customer has consented to receive communication via SMS."
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "List of categories or tags associated with the customer (e.g. VIP, Student).",
            "nullable": true
          },
          "socialSecurityNumber": {
            "type": "string",
            "description": "Social security number or personal identifier of the customer.",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "Postal code of the customer's address.",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "City of the customer's address.",
            "nullable": true
          },
          "birthday": {
            "type": "string",
            "description": "Customer's date of birth, typically in ISO 8601 format (YYYY-MM-DD).",
            "nullable": true
          },
          "addressLine1": {
            "type": "string",
            "description": "Primary address line of the customer's address (e.g. street name and number).",
            "nullable": true
          },
          "addressLine2": {
            "type": "string",
            "description": "Secondary address line (e.g. apartment number, building, etc.).",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "PostInventoryInputModel": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "description": "Specifies the product id to which the inventory event applies.",
            "format": "uuid"
          },
          "locationId": {
            "type": "string",
            "description": "Specifies the location id to which the inventory event applies.",
            "format": "uuid"
          },
          "quantity": {
            "type": "integer",
            "description": "Specifies the quantity change for the inventory event.",
            "format": "int32"
          },
          "type": {
            "enum": [
              0,
              1,
              2,
              3,
              4,
              5,
              6
            ],
            "type": "integer",
            "description": "Specifies the type of inventory event. (Sales = 5)",
            "format": "int32"
          },
          "reference": {
            "maxLength": 255,
            "type": "string",
            "description": "Specifies an optional reference for the inventory event. Maximum length is 255 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Input model for posting an inventory event for a specific product and location."
      },
      "ProductViewModel": {
        "type": "object",
        "properties": {
          "productId": {
            "type": "string",
            "description": "Unique identifier of the product.",
            "format": "uuid"
          },
          "productName": {
            "type": "string",
            "description": "Name of the product.",
            "nullable": true
          },
          "supplierId": {
            "type": "string",
            "description": "Unique identifier of the product's supplier.",
            "format": "uuid"
          },
          "supplierName": {
            "type": "string",
            "description": "Name of the supplier providing the product.",
            "nullable": true
          },
          "barcode": {
            "type": "string",
            "description": "Barcode of the product.",
            "nullable": true
          },
          "stock": {
            "type": "integer",
            "description": "Latest calcuclated stock of the product.",
            "format": "int32"
          },
          "salonId": {
            "type": "string",
            "description": "Name of salon where product is located",
            "format": "uuid"
          }
        },
        "additionalProperties": false
      },
      "ResourceAvailabilityViewModel": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "The date the availability information applies to.",
            "format": "date-time"
          },
          "bookableMinutes": {
            "type": "integer",
            "description": "Total number of minutes the resource is available for booking on the given date.",
            "format": "int32"
          },
          "bookedMinutes": {
            "type": "integer",
            "description": "Total number of minutes already booked on the given date.",
            "format": "int32"
          },
          "absenceMinutes": {
            "type": "integer",
            "description": "Total number of minutes the resource is marked absent on the given date.",
            "format": "int32"
          },
          "lunchMinutes": {
            "type": "integer",
            "description": "Number of minutes reserved for lunch on the given date.",
            "format": "int32"
          },
          "attendanceMinutes": {
            "type": "integer",
            "description": "Total number of minutes the resource is scheduled to be present (working) on the given date.",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "ResourceViewModel": {
        "type": "object",
        "properties": {
          "salonId": {
            "type": "string",
            "description": "Unique identifier of the salon the resource is associated with.",
            "format": "uuid"
          },
          "salonName": {
            "type": "string",
            "description": "Name of the salon.",
            "nullable": true
          },
          "resourceId": {
            "type": "string",
            "description": "Unique identifier of the resource.",
            "format": "uuid"
          },
          "resourceName": {
            "type": "string",
            "description": "Full name of the resource.",
            "nullable": true
          },
          "resourceNickName": {
            "type": "string",
            "description": "Optional nickname of the resource.",
            "nullable": true
          },
          "finishDate": {
            "type": "string",
            "description": "The date when the resource stopped being available, if applicable.",
            "format": "date-time",
            "nullable": true
          },
          "bookableOnline": {
            "type": "boolean",
            "description": "Indicates whether the resource is available for online booking."
          },
          "onlineTitle": {
            "type": "string",
            "description": "Title or label shown to customers during online booking.",
            "nullable": true
          },
          "availabilities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResourceAvailabilityViewModel"
            },
            "description": "List of availability details for the resource within the specified date range.",
            "nullable": true
          },
          "priceListName": {
            "type": "string",
            "description": "Name of the price list associated with the resource.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SaleHeaderViewModel": {
        "type": "object",
        "properties": {
          "receiptDate": {
            "type": "string",
            "description": "Date and time when the receipt was issued.",
            "format": "date-time",
            "nullable": true
          },
          "receiptType": {
            "enum": [
              0,
              1,
              2,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13
            ],
            "type": "integer",
            "description": "Type of receipt, such as sale, refund, or correction.",
            "format": "int32"
          },
          "receiptNumber": {
            "type": "string",
            "description": "Receipt number as shown on the printed receipt.",
            "nullable": true
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SaleRowViewModel"
            },
            "description": "List of individual sale items (products/services) in this receipt.",
            "nullable": true
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SaleRowPaymentViewModel"
            },
            "description": "List of payment methods and amounts used in the transaction.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SaleRowPaymentViewModel": {
        "type": "object",
        "properties": {
          "paymentType": {
            "enum": [
              0,
              1,
              2,
              3,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13,
              14,
              15,
              16,
              17,
              18,
              19,
              20,
              30,
              31,
              32,
              33,
              34,
              35,
              36,
              100,
              101,
              110,
              120,
              200,
              201,
              202,
              203,
              204,
              205,
              206,
              208,
              209,
              300,
              310,
              320,
              330,
              340,
              350,
              400,
              500
            ],
            "type": "integer",
            "description": "Type of payment (e.g., cash, card, voucher).",
            "format": "int32"
          },
          "amount": {
            "type": "number",
            "description": "Amount paid using the specified payment type.",
            "format": "double"
          }
        },
        "additionalProperties": false
      },
      "SaleRowViewModel": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "string",
            "description": "Internal identifier of the sold item (product or service).",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Name of the sold item.",
            "nullable": true
          },
          "number": {
            "type": "string",
            "description": "Article or product number.",
            "nullable": true
          },
          "barCode": {
            "type": "string",
            "description": "Barcode associated with the item.",
            "nullable": true
          },
          "customerId": {
            "type": "string",
            "description": "Identifier of the customer associated with the sale, if applicable.",
            "format": "uuid",
            "nullable": true
          },
          "customerName": {
            "type": "string",
            "description": "Name of the customer associated with the sale, if available.",
            "nullable": true
          },
          "type": {
            "enum": [
              0,
              1,
              2,
              3,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13,
              21,
              22,
              24,
              500
            ],
            "type": "integer",
            "description": "Type of row (e.g., product, service, discount).",
            "format": "int32"
          },
          "priceIncVat": {
            "type": "number",
            "description": "Unit price including VAT.",
            "format": "double"
          },
          "discount": {
            "type": "number",
            "description": "Discount applied to the item, if any.",
            "format": "double"
          },
          "totalPriceIncVat": {
            "type": "number",
            "description": "Total price for this item row, including VAT and after discount.",
            "format": "double"
          },
          "vatRate": {
            "type": "number",
            "description": "VAT rate applied to the item (e.g. 25 for 25% VAT).",
            "format": "double"
          },
          "bookingId": {
            "type": "string",
            "description": "ID of the booking associated with the sale, if the sale is related to a booking.",
            "format": "uuid",
            "nullable": true
          },
          "resourceId": {
            "type": "string",
            "description": "ID of the resource (e.g. staff member) involved in the sale or service.",
            "format": "uuid"
          },
          "resourceName": {
            "type": "string",
            "description": "Full name of the resource.",
            "nullable": true
          },
          "resourceNickName": {
            "type": "string",
            "description": "Optional nickname of the resource.",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "description": "Quantity of the item sold.",
            "format": "int32"
          },
          "rowId": {
            "type": "integer",
            "description": "SortIndex of the row",
            "format": "int32"
          }
        },
        "additionalProperties": false
      },
      "SaleViewModel": {
        "type": "object",
        "properties": {
          "salonId": {
            "type": "string",
            "description": "Unique identifier of the salon associated with the sales data.",
            "format": "uuid"
          },
          "salonName": {
            "type": "string",
            "description": "Name of the salon.",
            "nullable": true
          },
          "headers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SaleHeaderViewModel"
            },
            "description": "List of sales transaction headers, each representing a receipt.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "SalonViewModel": {
        "type": "object",
        "properties": {
          "salonId": {
            "type": "string",
            "format": "uuid"
          },
          "salonName": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ServiceViewModel": {
        "type": "object",
        "properties": {
          "serviceId": {
            "type": "string",
            "description": "Unique identifier of the service.",
            "format": "uuid"
          },
          "serviceName": {
            "type": "string",
            "description": "Name of the service.",
            "nullable": true
          },
          "isAddon": {
            "type": "boolean",
            "description": "Indicates whether the service is an add-on to another service."
          }
        },
        "additionalProperties": false
      }
    }
  },
  "tags": [
    {
      "name": "Getting started",
      "description": "To get started with the M3 external API, you need to obtain an API key. \n\nThis key is required for authentication and authorization to access the API endpoints.\n\nContact customer support to obtain your API key."
    },
    {
      "name": "Authentication",
      "description": "Access to this API requires authentication using an API key.\n\nInclude your API key in all requests by setting the `X-API-KEY` header:\n\n`X-API-KEY: your-api-key`"
    },
    {
      "name": "Rate Limiting",
      "description": "To prevent abuse and ensure equitable access for all users, each endpoint is subject to rate limiting.\n\nThe default rate limit is **10 requests per minute** per client IP address. This means that you can make up to 10 requests to any endpoint within a one-minute window.\n\nIf the rate limit is exceeded, the API will respond with HTTP status code **429 Too Many Requests**."
    },
    {
      "name": "Webhooks",
      "description": "The API supports webhooks for real-time notifications to external systems when specific events occur. \n\nYou can subscribe to various event types, including:\n\n* **Bookings created**\n\n* **Bookings updated**\n\n* **Bookings deleted**\n\n* **Customer no-show**\n\n* **Product stock changed**\n\nWebhook subscriptions can be configured at different levels of granularity:\n\n* **Portal-wide (all locations and staff)**\n\n* **Specific salon**\n\n* **Individual staff member**\n\nTo secure your webhook endpoint, you may specify a **custom authentication header** (e.g. `Webhook-Secret`) during configuration. This allows your system to validate that incoming requests originate from your own trusted webhook configuration.\n\nWebhook deliveries are sent via HTTP POST requests to a user-defined callback URL. Payloads contain structured JSON data relevant to the triggered event, enabling your system to process and respond to changes as they happen.\n\n**Example booking payload:**\n\n```json\n{\n  \"Id\": \"00000000-0000-0000-0000-000000000000\",\n  \"LocationId\": \"00000000-0000-0000-0000-000000000000\",\n  \"LocationName\": \"Test salon\",\n  \"PersonId\": \"00000000-0000-0000-0000-000000000000\",\n  \"PersonName\": \"Test person\",\n  \"ServiceId\": \"00000000-0000-0000-0000-000000000000\",\n  \"ServiceName\": \"Test service\",\n  \"BookingPrice\": 100.0,\n  \"BookingStartDate\": \"2025-01-01T09:00:00+01:00\",\n  \"BookingEndDate\": \"2025-01-01T10:00:00+01:00\",\n  \"Customer\": {\n    \"Id\": \"00000000-0000-0000-0000-000000000000\",\n    \"MobilePhoneNumber\": \"+46000000000\",\n    \"PhoneNumber\": \"+46000000000\",\n    \"FirstName\": \"Test\",\n    \"LastName\": \"Customer\",\n    \"EmailAdress\": \"test@test.se\",\n    \"SocialSecurityNumber\": \"199906123333\",\n    \"NewCustomer\": false\n  },\n  \"EventCreated\": \"2025-01-01T08:00:00+00:00\",\n  \"BookedOnline\": true,\n  \"Cancelled\": false,\n  \"NewCustomer\": false,\n  \"GclId\": null,\n  \"Referer\": \"bokadirekt.se\",\n  \"Note\": null\n}\n```\n\n**Example product stock change payload:**\n\n```json\n{\n  \"ProductId\": \"00000000-0000-0000-0000-000000000000\",\n  \"ProductName\": \"Test product\",\n  \"LocationId\": \"00000000-0000-0000-0000-000000000000\",\n  \"LocationName\": \"Test salon\",\n  \"PreviousStock\": 10,\n  \"CurrentStock\": 8,\n  \"ChangeAmount\": -2,\n  \"ChangeType\": \"Sale\",\n  \"Price\": 299.00,\n  \"VatRate\": 0.25,\n  \"EventCreated\": \"2025-01-01T08:00:00+00:00\"\n}\n```\nTo enable webhooks contact customer support."
    }
  ]
}