{
  "openapi": "3.1.0",
  "info": {
    "title": "Setora REST API",
    "version": "1.0.0-alpha",
    "description": "The Setora REST API exposes availability, bookings, clients, services and staff for AI receptionists and third-party integrations. All paths are relative to the base URL."
  },
  "servers": [
    {
      "url": "https://rest.setora.co.uk/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/locations": {
      "get": {
        "operationId": "listLocations",
        "summary": "List locations",
        "description": "List the locations belonging to the authenticated organisation. Returns active locations only.",
        "tags": ["Locations"],
        "parameters": [
          { "$ref": "#/components/parameters/cursor" }
        ],
        "responses": {
          "200": {
            "description": "A list of locations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Location" }
                    },
                    "next_cursor": { "type": "string" }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "4f8d3a91-2b6c-4e7f-9a01-1c5d8e2f6b3a",
                      "slug": "soho",
                      "name": "Soho",
                      "timezone": "Europe/London",
                      "street_address": "12 Berwick Street",
                      "city": "London",
                      "postal_code": "W1F 0PT",
                      "country": "GB",
                      "phone": "+44 20 7946 0958"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/locations/{slug}/services": {
      "get": {
        "operationId": "listLocationServices",
        "summary": "List services at a location",
        "description": "List the services bookable at a location, including pricing, duration, deposit requirement, and which staff can deliver each service.",
        "tags": ["Locations"],
        "parameters": [
          { "$ref": "#/components/parameters/locationSlug" },
          { "$ref": "#/components/parameters/cursor" }
        ],
        "responses": {
          "200": {
            "description": "A list of services.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Service" }
                    },
                    "next_cursor": { "type": "string" }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15",
                      "name": "Haircut",
                      "duration_minutes": 30,
                      "price": { "amount": 2500, "currency": "GBP" },
                      "deposit": { "amount": 500, "currency": "GBP" },
                      "category": "Hair",
                      "online_bookable": true,
                      "staff": [
                        { "id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84", "name": "Kai" },
                        { "id": "c1d4f8a2-6e3b-49d7-a085-2b8c5f1e7d39", "name": "Jordan" }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/locations/{slug}/staff": {
      "get": {
        "operationId": "listLocationStaff",
        "summary": "List staff at a location",
        "description": "List active staff at a location. Each staff member returns the services they can deliver.",
        "tags": ["Locations"],
        "parameters": [
          { "$ref": "#/components/parameters/locationSlug" },
          { "$ref": "#/components/parameters/cursor" }
        ],
        "responses": {
          "200": {
            "description": "A list of staff members.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Staff" }
                    },
                    "next_cursor": { "type": "string" }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84",
                      "name": "Kai",
                      "service_ids": [
                        "2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15",
                        "e8b1d4f2-3a9c-47e5-bd06-4c7f2a1e8b39"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/locations/{slug}/availability": {
      "get": {
        "operationId": "getAvailability",
        "summary": "Get availability",
        "description": "Return bookable slots for one or more services at a location. Slots are returned in the location's local timezone with a UTC offset.",
        "tags": ["Availability"],
        "parameters": [
          { "$ref": "#/components/parameters/locationSlug" },
          {
            "name": "service_ids",
            "in": "query",
            "required": true,
            "description": "Comma-separated service UUIDs.",
            "schema": { "type": "string" }
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Local date to start the search from (YYYY-MM-DD).",
            "schema": { "type": "string", "format": "date" },
            "example": "2026-05-12"
          },
          {
            "name": "days",
            "in": "query",
            "description": "Number of days to search forward (1–14). Defaults to 1.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 14, "default": 1 }
          },
          {
            "name": "staff_id",
            "in": "query",
            "description": "Restrict results to a specific staff member.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Available slots.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/AvailabilityResult" }
                  }
                },
                "example": {
                  "data": {
                    "location_slug": "soho",
                    "timezone": "Europe/London",
                    "service_ids": ["2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15"],
                    "duration": { "minutes": 30, "display": "30 mins" },
                    "slots": [
                      {
                        "starts_at": "2026-05-12T09:00:00+01:00",
                        "ends_at": "2026-05-12T09:30:00+01:00",
                        "staff_id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84"
                      },
                      {
                        "starts_at": "2026-05-12T09:30:00+01:00",
                        "ends_at": "2026-05-12T10:00:00+01:00",
                        "staff_id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/clients": {
      "get": {
        "operationId": "lookupClient",
        "summary": "Look up a client",
        "description": "Look up a client by phone or email. At least one parameter is required.",
        "tags": ["Clients"],
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "description": "E.164 phone number (e.g. +447700900123).",
            "schema": { "type": "string" }
          },
          {
            "name": "email",
            "in": "query",
            "description": "Email address.",
            "schema": { "type": "string", "format": "email" }
          }
        ],
        "responses": {
          "200": {
            "description": "Client found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Client" }
                  }
                },
                "example": {
                  "data": {
                    "id": "5d9f2a18-7c4e-46b3-a821-3e0b4c8d1f72",
                    "first_name": "Alex",
                    "last_name": "Carter",
                    "phone": "+447700900123",
                    "email": "alex@example.com",
                    "marketing_consent": false,
                    "created_at": "2025-11-04T10:22:00Z"
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "operationId": "createClient",
        "summary": "Create a client",
        "description": "Create a new client. At least one of phone or email is required. If a matching phone or email already exists, the existing client is returned with 200 rather than a duplicate created.",
        "tags": ["Clients"],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["first_name", "last_name"],
                "properties": {
                  "first_name": { "type": "string", "minLength": 1, "maxLength": 60 },
                  "last_name": { "type": "string", "minLength": 1, "maxLength": 60 },
                  "phone": { "type": "string", "description": "E.164 format." },
                  "email": { "type": "string", "format": "email" },
                  "marketing_consent": { "type": "boolean", "default": false }
                }
              },
              "example": {
                "first_name": "Alex",
                "last_name": "Carter",
                "phone": "+447700900123",
                "email": "alex@example.com"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Client" }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Existing client returned (duplicate prevented).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Client" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/bookings": {
      "post": {
        "operationId": "createBooking",
        "summary": "Create a booking",
        "description": "Create a booking. If the service requires a deposit, the response includes a hosted payment_url. The booking is held in pending_payment until the deposit is paid. Pass an Idempotency-Key for safe retries.\n\nREST v1 does not support Booking Protection: requests to create a booking at a location where `booking_protection_policy.applies` is `true` return `409 booking_protection_required`. Use a Setora-hosted booking flow for protected locations.",
        "tags": ["Bookings"],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "UUID v4 for safe retries. Stored for 24 hours.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["location_slug", "service_ids", "client_id", "starts_at"],
                "properties": {
                  "location_slug": { "type": "string" },
                  "service_ids": {
                    "type": "array",
                    "items": { "type": "string", "format": "uuid" },
                    "minItems": 1
                  },
                  "staff_id": { "type": "string", "format": "uuid", "description": "Omit for any-staff assignment." },
                  "client_id": { "type": "string", "format": "uuid" },
                  "starts_at": { "type": "string", "format": "date-time", "description": "Must match a slot from /availability." },
                  "notes": { "type": "string", "maxLength": 1000, "description": "Free text shown to staff in the Hub." }
                }
              },
              "example": {
                "location_slug": "soho",
                "service_ids": ["2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15"],
                "client_id": "5d9f2a18-7c4e-46b3-a821-3e0b4c8d1f72",
                "starts_at": "2026-05-12T09:30:00+01:00"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booking created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": {
                      "allOf": [
                        { "$ref": "#/components/schemas/Booking" },
                        {
                          "type": "object",
                          "properties": {
                            "deposit": { "$ref": "#/components/schemas/Money", "nullable": true },
                            "payment_url": { "type": "string", "format": "uri", "nullable": true }
                          }
                        }
                      ]
                    }
                  }
                },
                "example": {
                  "data": {
                    "reference": "SET-A1B2C3",
                    "status": "pending_payment",
                    "location_slug": "soho",
                    "service_ids": ["2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15"],
                    "staff_id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84",
                    "client_id": "5d9f2a18-7c4e-46b3-a821-3e0b4c8d1f72",
                    "starts_at": "2026-05-12T09:30:00+01:00",
                    "ends_at": "2026-05-12T10:00:00+01:00",
                    "notes": null,
                    "created_at": "2026-04-30T15:01:22Z",
                    "updated_at": "2026-04-30T15:01:22Z",
                    "deposit": { "amount": 500, "currency": "GBP" },
                    "payment_url": "https://checkout.stripe.com/c/pay/cs_live_..."
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/bookings/{reference}": {
      "get": {
        "operationId": "getBooking",
        "summary": "Get a booking",
        "description": "Fetch a single booking by reference, including current status.",
        "tags": ["Bookings"],
        "parameters": [
          { "$ref": "#/components/parameters/bookingReference" }
        ],
        "responses": {
          "200": {
            "description": "Booking found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Booking" }
                  }
                },
                "example": {
                  "data": {
                    "reference": "SET-A1B2C3",
                    "status": "confirmed",
                    "location_slug": "soho",
                    "service_ids": ["2a7c9e34-58b1-41f6-8d92-6e4a3b0c7d15"],
                    "staff_id": "9b3e7d2c-4a1f-48e6-bc52-7f0a9d3b1e84",
                    "client_id": "5d9f2a18-7c4e-46b3-a821-3e0b4c8d1f72",
                    "starts_at": "2026-05-12T09:30:00+01:00",
                    "ends_at": "2026-05-12T10:00:00+01:00",
                    "notes": null,
                    "created_at": "2026-04-30T15:01:22Z",
                    "updated_at": "2026-04-30T15:02:08Z"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "operationId": "rescheduleBooking",
        "summary": "Reschedule a booking",
        "description": "Reschedule a booking. The new start must be a valid slot for the same service at the same location. Cancellation policy windows are enforced.",
        "tags": ["Bookings"],
        "parameters": [
          { "$ref": "#/components/parameters/bookingReference" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["starts_at"],
                "properties": {
                  "starts_at": { "type": "string", "format": "date-time" },
                  "staff_id": { "type": "string", "format": "uuid", "description": "Reassign to a different eligible staff member." }
                }
              },
              "example": {
                "starts_at": "2026-05-12T11:00:00+01:00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booking rescheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/Booking" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/bookings/{reference}/cancel": {
      "post": {
        "operationId": "cancelBooking",
        "summary": "Cancel a booking",
        "description": "Cancel a booking. The shop's cancellation policy controls whether a deposit is refunded.",
        "tags": ["Bookings"],
        "parameters": [
          { "$ref": "#/components/parameters/bookingReference" }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": { "type": "string", "maxLength": 1000, "description": "Free text shown to staff." }
                }
              },
              "example": {
                "reason": "Client requested cancellation"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booking cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["data"],
                  "properties": {
                    "data": { "$ref": "#/components/schemas/CancellationResult" }
                  }
                },
                "example": {
                  "data": {
                    "reference": "SET-A1B2C3",
                    "status": "cancelled",
                    "cancelled_at": "2026-05-11T18:14:00Z",
                    "deposit_refunded": false
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued from the Hub (sk_live_*)."
      }
    },
    "parameters": {
      "locationSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "URL-safe location identifier.",
        "schema": { "type": "string" },
        "example": "soho"
      },
      "bookingReference": {
        "name": "reference",
        "in": "path",
        "required": true,
        "description": "Human-readable booking reference (e.g. SET-A1B2C3).",
        "schema": { "type": "string" },
        "example": "SET-A1B2C3"
      },
      "cursor": {
        "name": "cursor",
        "in": "query",
        "description": "Opaque pagination cursor from a previous response's next_cursor.",
        "schema": { "type": "string" }
      }
    },
    "schemas": {
      "Location": {
        "type": "object",
        "required": ["id", "slug", "name", "timezone", "booking_protection_policy"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "slug": { "type": "string" },
          "name": { "type": "string" },
          "timezone": { "type": "string", "description": "IANA timezone identifier." },
          "street_address": { "type": "string" },
          "city": { "type": "string" },
          "postal_code": { "type": "string" },
          "country": { "type": "string", "description": "ISO 3166-1 two-letter code." },
          "phone": { "type": "string", "nullable": true, "description": "Customer-facing contact phone number for the location." },
          "booking_protection_policy": { "$ref": "#/components/schemas/BookingProtectionPolicy" }
        }
      },
      "Service": {
        "type": "object",
        "required": ["id", "name", "duration_minutes", "price", "online_bookable", "staff"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "duration_minutes": { "type": "integer" },
          "price": { "$ref": "#/components/schemas/Money" },
          "deposit": { "$ref": "#/components/schemas/Money", "nullable": true, "description": "null when no deposit is needed." },
          "category": { "type": "string", "nullable": true },
          "online_bookable": { "type": "boolean" },
          "staff": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["id", "name"],
              "properties": {
                "id": { "type": "string", "format": "uuid" },
                "name": { "type": "string" }
              }
            }
          }
        }
      },
      "Staff": {
        "type": "object",
        "required": ["id", "name", "service_ids"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "service_ids": {
            "type": "array",
            "items": { "type": "string", "format": "uuid" }
          }
        }
      },
      "AvailabilityResult": {
        "type": "object",
        "required": ["location_slug", "timezone", "service_ids", "duration", "slots"],
        "properties": {
          "location_slug": { "type": "string" },
          "timezone": { "type": "string" },
          "service_ids": {
            "type": "array",
            "items": { "type": "string", "format": "uuid" }
          },
          "duration": {
            "type": "object",
            "required": ["minutes", "display"],
            "properties": {
              "minutes": { "type": "integer" },
              "display": { "type": "string" }
            }
          },
          "slots": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["starts_at", "ends_at", "staff_id"],
              "properties": {
                "starts_at": { "type": "string", "format": "date-time" },
                "ends_at": { "type": "string", "format": "date-time" },
                "staff_id": { "type": "string", "format": "uuid" }
              }
            }
          }
        }
      },
      "Client": {
        "type": "object",
        "required": ["id", "first_name", "last_name", "marketing_consent", "created_at"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "phone": { "type": "string", "nullable": true, "description": "E.164 format." },
          "email": { "type": "string", "format": "email", "nullable": true },
          "marketing_consent": { "type": "boolean" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "Booking": {
        "type": "object",
        "required": ["reference", "status", "location_slug", "service_ids", "staff_id", "client_id", "starts_at", "ends_at", "booking_protection", "created_at", "updated_at"],
        "properties": {
          "reference": { "type": "string", "description": "Human-readable reference (e.g. SET-A1B2C3)." },
          "status": {
            "type": "string",
            "enum": ["pending_payment", "pending_confirmation", "confirmed", "needs_checkout", "completed", "no_show", "cancelled"]
          },
          "location_slug": { "type": "string" },
          "service_ids": {
            "type": "array",
            "items": { "type": "string", "format": "uuid" }
          },
          "staff_id": { "type": "string", "format": "uuid" },
          "client_id": { "type": "string", "format": "uuid" },
          "starts_at": { "type": "string", "format": "date-time", "description": "Local timezone with UTC offset." },
          "ends_at": { "type": "string", "format": "date-time", "description": "Local timezone with UTC offset." },
          "notes": { "type": "string", "nullable": true },
          "booking_protection": { "$ref": "#/components/schemas/BookingProtection" },
          "created_at": { "type": "string", "format": "date-time", "description": "UTC." },
          "updated_at": { "type": "string", "format": "date-time", "description": "UTC." }
        }
      },
      "BookingProtectionPolicy": {
        "type": "object",
        "description": "Read-only summary of the Booking Protection policy at a location. REST v1 cannot create the card setup that backs these fees; bookings at locations where `applies` is `true` must be created via a Setora-hosted flow.",
        "required": ["applies", "late_cancellation_fee_percentage", "no_show_fee_percentage"],
        "properties": {
          "applies": { "type": "boolean", "description": "Whether Booking Protection is enabled for this location." },
          "late_cancellation_fee_percentage": { "type": "integer", "nullable": true, "description": "Percentage of the booking total chargeable on late cancellation. null when protection does not apply." },
          "no_show_fee_percentage": { "type": "integer", "nullable": true, "description": "Percentage of the booking total chargeable on no-show. null when protection does not apply." }
        }
      },
      "BookingProtection": {
        "type": "object",
        "description": "Read-only snapshot of Booking Protection on a booking. REST v1 cannot initiate or confirm the protected card setup; these fields reflect the state of flows triggered through Setora-hosted surfaces.",
        "required": ["applies"],
        "properties": {
          "applies": { "type": "boolean", "description": "Whether the booking is covered by Booking Protection. All other fields are omitted when this is `false`." },
          "policy": {
            "type": "object",
            "description": "Fee percentages snapshotted at booking creation.",
            "required": ["late_cancellation_fee_percentage", "no_show_fee_percentage"],
            "properties": {
              "late_cancellation_fee_percentage": { "type": "integer", "nullable": true },
              "no_show_fee_percentage": { "type": "integer", "nullable": true }
            }
          },
          "setup": {
            "type": "object",
            "description": "State of the protected card setup. REST v1 cannot create this; values reflect setup initiated through Setora-hosted flows.",
            "required": ["status", "payment_method_saved", "completed_at"],
            "properties": {
              "status": {
                "type": "string",
                "nullable": true,
                "enum": ["pending", "failed", "succeeded", null]
              },
              "payment_method_saved": { "type": "boolean" },
              "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "UTC." }
            }
          },
          "late_cancellation_fee": {
            "$ref": "#/components/schemas/BookingProtectionFeeOutcome",
            "nullable": true,
            "description": "Outcome of the late cancellation fee, or null when no fee has been processed."
          },
          "no_show_fee": {
            "$ref": "#/components/schemas/BookingProtectionFeeOutcome",
            "nullable": true,
            "description": "Outcome of the no-show fee, or null when no fee has been processed."
          }
        }
      },
      "BookingProtectionFeeOutcome": {
        "type": "object",
        "required": ["status", "amount_minor", "charge_amount_minor", "deposit_credit_minor", "currency", "processed_at"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["charged", "waived", "deposit_covered", "failed"]
          },
          "amount_minor": { "type": "integer", "nullable": true, "description": "Total fee in minor units before deposit credit." },
          "charge_amount_minor": { "type": "integer", "nullable": true, "description": "Amount actually charged to the saved card after deposit credit." },
          "deposit_credit_minor": { "type": "integer", "nullable": true, "description": "Portion of the fee covered by the held deposit." },
          "currency": { "type": "string", "nullable": true, "description": "ISO 4217 code." },
          "processed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "UTC." }
        }
      },
      "CancellationResult": {
        "type": "object",
        "required": ["reference", "status", "cancelled_at", "deposit_refunded"],
        "properties": {
          "reference": { "type": "string" },
          "status": { "type": "string", "const": "cancelled" },
          "cancelled_at": { "type": "string", "format": "date-time" },
          "deposit_refunded": { "type": "boolean" }
        }
      },
      "Money": {
        "type": "object",
        "required": ["amount", "currency"],
        "properties": {
          "amount": { "type": "integer", "description": "Minor units (e.g. pence for GBP)." },
          "currency": { "type": "string", "description": "ISO 4217 code." }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": { "type": "string" },
              "message": { "type": "string" },
              "param": { "type": "string" }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed JSON or a parameter failed validation.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "invalid_request", "message": "The service_ids field is required.", "param": "service_ids" }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "invalid_api_key", "message": "The provided API key is invalid or has been revoked.", "param": "Authorization" }
            }
          }
        }
      },
      "Forbidden": {
        "description": "Key scope does not allow this action.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "insufficient_scope", "message": "The key is valid but its scope does not allow this action.", "param": "Authorization" }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "not_found", "message": "The requested resource was not found." }
            }
          }
        }
      },
      "Conflict": {
        "description": "Slot unavailable, policy violation, or idempotency key reused.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": {
              "error": { "code": "conflict", "message": "The selected slot is no longer available.", "param": "starts_at" }
            }
          }
        }
      }
    }
  }
}
