{
  "openapi": "3.0.1",
  "info": {
    "title": "Barberly Booking API",
    "description": "Public API for browsing salons and creating bookings.",
    "version": "1.0"
  },
  "paths": {
    "/v1/accounts/register": {
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Register customer",
        "description": "Starts registration. A confirmation code is sent to the customer, by SMS or by email\r\naccording to the confirmation type configured in your booking settings. Complete the\r\nregistration by posting the code to `register/confirm`.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RegisterCustomerRequest"
                  }
                ],
                "description": "Starts registration. Both the phone number and the email address are always required,\r\nregardless of how the confirmation code is delivered."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RegisterCustomerRequest"
                  }
                ],
                "description": "Starts registration. Both the phone number and the email address are always required,\r\nregardless of how the confirmation code is delivered."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RegisterCustomerRequest"
                  }
                ],
                "description": "Starts registration. Both the phone number and the email address are always required,\r\nregardless of how the confirmation code is delivered."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/RegisterCustomerRequest"
                  }
                ],
                "description": "Starts registration. Both the phone number and the email address are always required,\r\nregardless of how the confirmation code is delivered."
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/register/confirm": {
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Confirm registration",
        "description": "Completes registration with the code that was delivered and returns the\r\n`customerId` to use when booking.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmRegistrationRequest"
                  }
                ],
                "description": "Completes registration. The account is always identified by phone number here, even when\r\nthe confirmation code itself was delivered by email."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmRegistrationRequest"
                  }
                ],
                "description": "Completes registration. The account is always identified by phone number here, even when\r\nthe confirmation code itself was delivered by email."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmRegistrationRequest"
                  }
                ],
                "description": "Completes registration. The account is always identified by phone number here, even when\r\nthe confirmation code itself was delivered by email."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmRegistrationRequest"
                  }
                ],
                "description": "Completes registration. The account is always identified by phone number here, even when\r\nthe confirmation code itself was delivered by email."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/login": {
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Log in",
        "description": "Authenticates a customer by phone number and returns the `customerId` to use\r\nwhen booking.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ],
                "description": "Authenticates an existing customer."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ],
                "description": "Authenticates an existing customer."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ],
                "description": "Authenticates an existing customer."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/LoginRequest"
                  }
                ],
                "description": "Authenticates an existing customer."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/password/reset": {
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Reset password",
        "description": "Starts a password reset. Set `recipient` to the customer's email address if your\r\nbooking settings confirm by email, or to their phone number if they confirm by SMS.\r\nThe confirmation code is sent to that recipient.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResetPasswordRequest"
                  }
                ],
                "description": "Starts a password reset. A confirmation code is sent to Barberly.Public.Api.Contracts.v1.ResetPasswordRequest.Recipient."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResetPasswordRequest"
                  }
                ],
                "description": "Starts a password reset. A confirmation code is sent to Barberly.Public.Api.Contracts.v1.ResetPasswordRequest.Recipient."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResetPasswordRequest"
                  }
                ],
                "description": "Starts a password reset. A confirmation code is sent to Barberly.Public.Api.Contracts.v1.ResetPasswordRequest.Recipient."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ResetPasswordRequest"
                  }
                ],
                "description": "Starts a password reset. A confirmation code is sent to Barberly.Public.Api.Contracts.v1.ResetPasswordRequest.Recipient."
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/password/reset/confirm": {
      "post": {
        "tags": [
          "Accounts"
        ],
        "summary": "Confirm password reset",
        "description": "Completes a password reset with the code that was delivered. The new password sent to\r\n`password/reset` becomes active once this call succeeds.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmResetPasswordRequest"
                  }
                ],
                "description": "Completes a password reset with the code that was delivered."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmResetPasswordRequest"
                  }
                ],
                "description": "Completes a password reset with the code that was delivered."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmResetPasswordRequest"
                  }
                ],
                "description": "Completes a password reset with the code that was delivered."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/ConfirmResetPasswordRequest"
                  }
                ],
                "description": "Completes a password reset with the code that was delivered."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthenticationResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Create booking",
        "description": "Depending on the salon's settings the booking may come back unconfirmed, meaning it\r\nwas accepted as a request awaiting the salon's approval. Some salons require the\r\ncustomer to be registered - pass a `customerId` obtained from the accounts\r\nendpoints in that case.",
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateBookingRequest"
                  }
                ]
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateBookingRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateBookingRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CreateBookingRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingModel"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}": {
      "put": {
        "tags": [
          "Bookings"
        ],
        "summary": "Update booking (reschedule)",
        "description": "Full replacement of an existing booking - send every field again, not just the ones\r\nthat changed. Commonly used to reschedule (change the time slot or team member), but\r\nany field may be updated, including the services performed.\r\n            \r\nThe salon may refuse the change - for example if a minimum cancellation/edit notice\r\nhas already passed - in which case this returns 400 with the specific reason.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/UpdateBookingRequest"
                  }
                ],
                "description": "Full replacement for an existing booking - the same shape as Barberly.Public.Api.Contracts.v1.CreateBookingRequest,\r\nsince the salon requires every field again rather than merging a partial patch. Commonly used\r\nto reschedule (change the time slot or team member) but any field may change, including the\r\nservices performed. Subject to the salon's own edit rules (e.g. a minimum notice period);\r\nsee the 400 response for the specific reason when a change is refused."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/UpdateBookingRequest"
                  }
                ],
                "description": "Full replacement for an existing booking - the same shape as Barberly.Public.Api.Contracts.v1.CreateBookingRequest,\r\nsince the salon requires every field again rather than merging a partial patch. Commonly used\r\nto reschedule (change the time slot or team member) but any field may change, including the\r\nservices performed. Subject to the salon's own edit rules (e.g. a minimum notice period);\r\nsee the 400 response for the specific reason when a change is refused."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/UpdateBookingRequest"
                  }
                ],
                "description": "Full replacement for an existing booking - the same shape as Barberly.Public.Api.Contracts.v1.CreateBookingRequest,\r\nsince the salon requires every field again rather than merging a partial patch. Commonly used\r\nto reschedule (change the time slot or team member) but any field may change, including the\r\nservices performed. Subject to the salon's own edit rules (e.g. a minimum notice period);\r\nsee the 400 response for the specific reason when a change is refused."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/UpdateBookingRequest"
                  }
                ],
                "description": "Full replacement for an existing booking - the same shape as Barberly.Public.Api.Contracts.v1.CreateBookingRequest,\r\nsince the salon requires every field again rather than merging a partial patch. Commonly used\r\nto reschedule (change the time slot or team member) but any field may change, including the\r\nservices performed. Subject to the salon's own edit rules (e.g. a minimum notice period);\r\nsee the 400 response for the specific reason when a change is refused."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingModel"
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Get booking",
        "description": "Returns a single booking by its id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingModel"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/cancel": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Cancel booking",
        "description": "Cancels a booking. Salons may set a minimum cancellation notice; a booking that is\r\nalready too close to its start time is refused. The body is optional: send it only to\r\nrecord a cancellation reason.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CancelBookingRequest"
                  }
                ]
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CancelBookingRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CancelBookingRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/CancelBookingRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "No Content"
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/locations/{locationId}/services": {
      "post": {
        "tags": [
          "Catalog"
        ],
        "summary": "List services",
        "description": "Lists the services that can still be booked at this location, grouped by category.\r\nPass whatever the customer has already chosen - a team member, a slot, or both -\r\nand the result is narrowed down accordingly. Services that belong to no category are\r\nreturned in a final group with no name.",
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ServiceCategoryModel"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/locations/{locationId}/team-members": {
      "post": {
        "tags": [
          "Catalog"
        ],
        "summary": "List team members",
        "description": "Lists the team members who can still take this booking at the location.\r\nPass the already-chosen services and/or slot to narrow the result.",
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailabilityQuery"
                  }
                ],
                "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TeamMemberModel"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/locations/{locationId}/availability/{year}/{month}": {
      "post": {
        "tags": [
          "Catalog"
        ],
        "summary": "Get availability",
        "description": "Returns the days of the given month together with their bookable slots. Days outside\r\nthe requested month are included to complete the calendar weeks and are flagged\r\naccordingly.",
        "parameters": [
          {
            "name": "locationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "year",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "month",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json-patch+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailableDaysQuery"
                  }
                ],
                "description": "Query for the days of a given month that still have availability."
              }
            },
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailableDaysQuery"
                  }
                ],
                "description": "Query for the days of a given month that still have availability."
              }
            },
            "text/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailableDaysQuery"
                  }
                ],
                "description": "Query for the days of a given month that still have availability."
              }
            },
            "application/*+json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AvailableDaysQuery"
                  }
                ],
                "description": "Query for the days of a given month that still have availability."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AvailableDayModel"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/v1/locations": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "Get all locations",
        "description": "Returns the list of bookable locations for the salon.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LocationModel"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/locations/{id}": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "Get location by id",
        "description": "Returns details for a single location identified by its GUID.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationModel"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AuthenticationResult": {
        "type": "object",
        "properties": {
          "customer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CustomerAccountModel"
              }
            ],
            "description": "An authenticated customer.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Result of a successful registration, login or password reset."
      },
      "AvailabilityQuery": {
        "type": "object",
        "properties": {
          "serviceIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Services already selected, if any. Use this for plain services; when a service was\r\nchosen as an add-on of another one, use Barberly.Public.Api.Contracts.v1.AvailabilityQuery.SelectedServices instead so\r\nthe parent relationship is preserved.",
            "nullable": true
          },
          "selectedServices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SelectedServiceModel"
            },
            "description": "Selected services with their category and parent, for catalogues that use add-ons.\r\nTakes precedence over Barberly.Public.Api.Contracts.v1.AvailabilityQuery.ServiceIds when both are supplied.",
            "nullable": true
          },
          "teamMemberId": {
            "type": "string",
            "description": "Team member already selected, if any.",
            "format": "uuid",
            "nullable": true
          },
          "timeSlot": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeSlotRequest"
              }
            ],
            "description": "Slot already selected, if any.",
            "nullable": true
          },
          "bookingId": {
            "type": "string",
            "description": "Set when modifying an existing booking so its own slot is not treated as taken.",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Carries what the customer has already chosen. Every field is optional: the caller may\r\nhave picked nothing beyond the location, or may already have a team member and a slot.\r\nThe response is narrowed down to what is still bookable given that selection."
      },
      "AvailableDayModel": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "nullable": true
          },
          "year": {
            "type": "integer",
            "format": "int32"
          },
          "month": {
            "type": "integer",
            "format": "int32"
          },
          "day": {
            "type": "integer",
            "format": "int32"
          },
          "dayOfWeek": {
            "enum": [
              "Sunday",
              "Monday",
              "Tuesday",
              "Wednesday",
              "Thursday",
              "Friday",
              "Saturday"
            ],
            "type": "string",
            "allOf": [
              {
                "$ref": "#/components/schemas/DayOfWeek"
              }
            ]
          },
          "isAvailable": {
            "type": "boolean",
            "description": "False when the day has no availability at all."
          },
          "timeSlots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimeSlotModel"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One calendar day with its bookable slots."
      },
      "AvailableDaysQuery": {
        "type": "object",
        "properties": {
          "serviceIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "nullable": true
          },
          "teamMemberId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "bookingId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Query for the days of a given month that still have availability."
      },
      "BookingModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "customerId": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "enum": [
              "Unconfirmed",
              "Confirmed",
              "Cancelled"
            ],
            "type": "string",
            "allOf": [
              {
                "$ref": "#/components/schemas/BookingStatus"
              }
            ],
            "description": "Current state of the booking, sent as a string: `Unconfirmed`, `Confirmed`\r\nor `Cancelled`. Treat an unrecognised value as a state added in a later release\r\nrather than as an error."
          },
          "isConfirmed": {
            "type": "boolean",
            "description": "Convenience flag; true only when the status is `Confirmed`. It is false while a\r\nbooking request is still waiting for the salon to approve it.",
            "readOnly": true
          },
          "timeSlot": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeSlotModel"
              }
            ],
            "description": "A bookable slot. Dates and times are in the salon's local time zone.",
            "nullable": true
          },
          "locationId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "teamMemberId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "serviceIds": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "number",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "BookingStatus": {
        "enum": [
          "Unconfirmed",
          "Confirmed",
          "Cancelled"
        ],
        "type": "string",
        "description": "State of the booking, sent as a string.\r\n            \r\n`Unconfirmed` - the salon requires a barber to approve bookings, so this is a booking\r\nrequest rather than a booking: the appointment is not held until the salon confirms it.\r\nOnly salons whose settings require approval ever produce this state.\r\n            \r\n`Confirmed` - the booking is made and the appointment stands.\r\n            \r\n`Cancelled` - the booking was cancelled and the appointment will not take place."
      },
      "CancelBookingRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ConfirmRegistrationRequest": {
        "required": [
          "confirmationCode",
          "phoneNumber"
        ],
        "type": "object",
        "properties": {
          "phoneNumber": {
            "minLength": 1,
            "type": "string",
            "description": "The phone number the registration was started with."
          },
          "confirmationCode": {
            "minLength": 1,
            "type": "string",
            "description": "The six digit code delivered by SMS or email."
          }
        },
        "additionalProperties": false,
        "description": "Completes registration. The account is always identified by phone number here, even when\r\nthe confirmation code itself was delivered by email."
      },
      "ConfirmResetPasswordRequest": {
        "required": [
          "confirmationCode",
          "recipient"
        ],
        "type": "object",
        "properties": {
          "recipient": {
            "minLength": 1,
            "type": "string",
            "description": "The same value that was sent as `recipient` when the reset was started: an email\r\naddress if your booking settings confirm by email, a phone number otherwise."
          },
          "confirmationCode": {
            "minLength": 1,
            "type": "string",
            "description": "The six digit code delivered by SMS or email."
          }
        },
        "additionalProperties": false,
        "description": "Completes a password reset with the code that was delivered."
      },
      "CreateBookingRequest": {
        "required": [
          "customer",
          "locationId",
          "serviceIds",
          "timeSlot"
        ],
        "type": "object",
        "properties": {
          "locationId": {
            "type": "string",
            "format": "uuid"
          },
          "teamMemberId": {
            "type": "string",
            "description": "Leave empty to let the salon assign a team member automatically.",
            "format": "uuid",
            "nullable": true
          },
          "serviceIds": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "timeSlot": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeSlotRequest"
              }
            ],
            "description": "A requested slot, in the salon's local time zone."
          },
          "customer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CustomerModel"
              }
            ]
          },
          "note": {
            "type": "string",
            "description": "Free-text note shown to the salon.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "CustomerAccountModel": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "Pass this as `customer.customerId` when creating a booking.",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "phoneNumber": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An authenticated customer."
      },
      "CustomerModel": {
        "required": [
          "firstName",
          "phoneNumber"
        ],
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "Existing customer id, when the caller already knows it.",
            "nullable": true
          },
          "firstName": {
            "minLength": 1,
            "type": "string"
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "format": "email",
            "nullable": true
          },
          "phoneNumber": {
            "minLength": 1,
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "DayOfWeek": {
        "enum": [
          "Sunday",
          "Monday",
          "Tuesday",
          "Wednesday",
          "Thursday",
          "Friday",
          "Saturday"
        ],
        "type": "string"
      },
      "LocationModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "streetAddress": {
            "type": "string",
            "nullable": true
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "zipCode": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "phoneNumber": {
            "type": "string",
            "nullable": true
          },
          "isMobile": {
            "type": "boolean"
          },
          "rating": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "reviewsCount": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "photoUrl": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "LoginRequest": {
        "required": [
          "password",
          "phoneNumber"
        ],
        "type": "object",
        "properties": {
          "phoneNumber": {
            "minLength": 1,
            "type": "string",
            "description": "The customer's phone number. Login is always by phone number, even for salons that\r\nconfirm by email."
          },
          "password": {
            "minLength": 1,
            "type": "string",
            "description": "The customer's password, always Base64-encoded."
          }
        },
        "additionalProperties": false,
        "description": "Authenticates an existing customer."
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": { }
      },
      "RegisterCustomerRequest": {
        "required": [
          "email",
          "firstName",
          "password",
          "phoneNumber"
        ],
        "type": "object",
        "properties": {
          "firstName": {
            "minLength": 1,
            "type": "string"
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "minLength": 1,
            "type": "string",
            "description": "Always required. Receives the confirmation code when the salon confirms by email.",
            "format": "email"
          },
          "phoneNumber": {
            "minLength": 1,
            "type": "string",
            "description": "Always required, in international format (for example +41764942928). Receives the\r\nconfirmation code when the salon confirms by SMS, and always identifies the account\r\nafterwards."
          },
          "password": {
            "minLength": 1,
            "type": "string",
            "description": "Password for the new account. Always send it Base64-encoded, so credentials do not\r\ntravel as plain text in logs or traces."
          }
        },
        "additionalProperties": false,
        "description": "Starts registration. Both the phone number and the email address are always required,\r\nregardless of how the confirmation code is delivered."
      },
      "ResetPasswordRequest": {
        "required": [
          "newPassword",
          "recipient"
        ],
        "type": "object",
        "properties": {
          "recipient": {
            "minLength": 1,
            "type": "string",
            "description": "The customer's email address if your booking settings confirm by email, otherwise\r\ntheir phone number."
          },
          "newPassword": {
            "minLength": 1,
            "type": "string",
            "description": "The new password, always Base64-encoded."
          }
        },
        "additionalProperties": false,
        "description": "Starts a password reset. A confirmation code is sent to Barberly.Public.Api.Contracts.v1.ResetPasswordRequest.Recipient."
      },
      "SelectedServiceModel": {
        "required": [
          "serviceId"
        ],
        "type": "object",
        "properties": {
          "serviceId": {
            "type": "string",
            "format": "uuid"
          },
          "parentServiceId": {
            "type": "string",
            "description": "Set when this service was chosen as an add-on of another one.",
            "format": "uuid",
            "nullable": true
          },
          "categoryId": {
            "type": "string",
            "description": "Category the service was picked from, when the catalogue is grouped.",
            "format": "uuid",
            "nullable": true
          },
          "duration": {
            "type": "integer",
            "description": "Overrides the catalogue duration, in minutes.",
            "format": "int32",
            "nullable": true
          },
          "price": {
            "type": "number",
            "description": "Overrides the catalogue price.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A service the customer has picked. Mirrors the internal SelectedServiceV3 so add-ons\r\nand per-selection price/duration overrides survive the round trip."
      },
      "ServiceCategoryModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Null for the catch-all group of services that have no category.",
            "format": "uuid",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "services": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceModel"
            },
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "ServiceModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "duration": {
            "type": "integer",
            "description": "Duration in minutes.",
            "format": "int32"
          },
          "photoUrl": {
            "type": "string",
            "nullable": true
          },
          "categoryId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "extras": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceModel"
            },
            "description": "Optional add-ons that can be booked together with this service.",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "TeamMemberModel": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "photoUrl": {
            "type": "string",
            "nullable": true
          },
          "thumbnailUrl": {
            "type": "string",
            "nullable": true
          },
          "defaultLocationId": {
            "type": "string",
            "format": "uuid"
          }
        },
        "additionalProperties": false,
        "description": "A bookable member of staff. Internally these are called \"employees\"/\"stylists\";\r\nthe public API always calls them team members."
      },
      "TimeSlotModel": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "Date of the slot, yyyy-MM-dd.",
            "nullable": true
          },
          "startMinutesOfDay": {
            "type": "integer",
            "description": "Minutes from midnight when the slot starts.",
            "format": "int32"
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Displayed length of the slot in minutes.",
            "format": "int32"
          },
          "timeFrom": {
            "type": "string",
            "description": "Start time formatted for display, following the salon's clock setting -\r\n\"16:20\" or \"04:20 PM\". Use Barberly.Public.Api.Contracts.v1.TimeSlotModel.StartMinutesOfDay for calculations.",
            "nullable": true
          },
          "timeTo": {
            "type": "string",
            "description": "End time formatted for display; see Barberly.Public.Api.Contracts.v1.TimeSlotModel.TimeFrom.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A bookable slot. Dates and times are in the salon's local time zone."
      },
      "TimeSlotRequest": {
        "required": [
          "date"
        ],
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "Calendar date of the slot, `yyyy-MM-dd` - for example `2017-07-21`. Send the\r\ndate only: this is not a timestamp, so do not send a time of day or a time zone offset.\r\nAnything after the date is ignored. The slot's time is taken from\r\n`startMinutesOfDay` and `durationMinutes`.",
            "format": "date",
            "example": "2017-07-21"
          },
          "startMinutesOfDay": {
            "maximum": 1440,
            "minimum": 0,
            "type": "integer",
            "description": "Minutes from midnight when the slot starts - `570` is 09:30.",
            "format": "int32"
          },
          "durationMinutes": {
            "maximum": 1440,
            "minimum": 0,
            "type": "integer",
            "description": "Length of the slot in minutes.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "A requested slot, in the salon's local time zone."
      },
      "UpdateBookingRequest": {
        "required": [
          "customer",
          "locationId",
          "serviceIds",
          "timeSlot"
        ],
        "type": "object",
        "properties": {
          "locationId": {
            "type": "string",
            "format": "uuid"
          },
          "teamMemberId": {
            "type": "string",
            "description": "Leave empty to let the salon assign a team member automatically.",
            "format": "uuid",
            "nullable": true
          },
          "serviceIds": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "timeSlot": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TimeSlotRequest"
              }
            ],
            "description": "A requested slot, in the salon's local time zone."
          },
          "customer": {
            "allOf": [
              {
                "$ref": "#/components/schemas/CustomerModel"
              }
            ]
          },
          "note": {
            "type": "string",
            "description": "Free-text note shown to the salon.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full replacement for an existing booking - the same shape as Barberly.Public.Api.Contracts.v1.CreateBookingRequest,\r\nsince the salon requires every field again rather than merging a partial patch. Commonly used\r\nto reschedule (change the time slot or team member) but any field may change, including the\r\nservices performed. Subject to the salon's own edit rules (e.g. a minimum notice period);\r\nsee the 400 response for the specific reason when a change is refused."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "description": "Your API key, issued from the management portal.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "security": [
    {
      "ApiKey": [ ]
    }
  ]
}