{
  "openapi": "3.0.0",
  "info": {
    "title": "Plek Public booking API",
    "description": "The routes under `/api/public/{org_slug}`: what an organisation's booking site and widgets show and book. No authentication; CORS is limited to the organisation's own websites, so call them from your server or from an allowed website.\n\nWalk through a booking in [The booking flow](https://development.plekbooking.com/guides/booking-flow).",
    "contact": {
      "name": "Plek",
      "url": "https://development.plekbooking.com"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.plekbooking.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Activities",
      "description": "Public endpoints for browsing activities"
    },
    {
      "name": "Booking form",
      "description": "What the booking form needs: custom fields, booking questions, booking window, cancellation policy, payment options"
    },
    {
      "name": "Bookings",
      "description": "Public endpoints for creating and managing bookings"
    },
    {
      "name": "Calendar",
      "description": "Public endpoints for calendar exports"
    },
    {
      "name": "Checkout",
      "description": "Booking: quote and place a checkout (sessions, series, parties and appointment moments)"
    },
    {
      "name": "Customer sign-in",
      "description": "Customer sign-in: email code, magic link, password, Google / Microsoft, refresh and sign-out"
    },
    {
      "name": "Discount Codes",
      "description": "Public endpoints for validating discount codes"
    },
    {
      "name": "Gift Cards",
      "description": "Public endpoints for gift card purchases and validation"
    },
    {
      "name": "Hire requests",
      "description": "Requesting a venue hire"
    },
    {
      "name": "Locations",
      "description": "Public endpoints for browsing locations"
    },
    {
      "name": "Organisation",
      "description": "Public endpoints for organisation information"
    },
    {
      "name": "Plans",
      "description": "Passes and subscriptions on sale"
    },
    {
      "name": "Preferences",
      "description": "The opt-out and unsubscribe links of emails, and a device stopping push notifications"
    },
    {
      "name": "Reviews",
      "description": "Published reviews of activities"
    },
    {
      "name": "Series",
      "description": "Public endpoints for browsing available series"
    },
    {
      "name": "Sessions",
      "description": "Public endpoints for browsing available sessions"
    },
    {
      "name": "Tags",
      "description": "Public endpoints for browsing tags"
    },
    {
      "name": "Terms",
      "description": "Public endpoints for terms and conditions"
    },
    {
      "name": "Waitlist",
      "description": "Public endpoints for joining waitlists"
    },
    {
      "name": "Widgets",
      "description": "Embeddable widgets (#659, #661): presets a website's snippet points at (staff), and what the embed loads (public)"
    }
  ],
  "paths": {
    "/api/public/{org_slug}/booking-window": {
      "get": {
        "tags": [
          "Booking form"
        ],
        "summary": "When can this be booked?",
        "description": "The booking window of a session or series now (#401, #341; never who is in the priority audience), its age requirement (#352, null without one) and, with an optional `Authorization: Bearer` customer token, whether a spot from the waiting list is kept for that customer (`heldForYou`, `heldSeats`, a party's `heldTickets`; #684, #796). A seat of next term kept for them (#736) shows the window as open. 404 for an unknown item.",
        "operationId": "publicGetBookingWindow",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "sessionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "seriesId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The state",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "bookingWindow": {
                      "$ref": "#/components/schemas/PublicFormBookingWindow"
                    },
                    "ageRequirement": {
                      "properties": {
                        "minMonths": {
                          "type": "integer",
                          "nullable": true
                        },
                        "maxMonths": {
                          "type": "integer",
                          "nullable": true
                        },
                        "enforcement": {
                          "type": "string"
                        },
                        "cutoff": {
                          "type": "string"
                        },
                        "referenceDate": {
                          "type": "string",
                          "format": "date"
                        }
                      },
                      "type": "object",
                      "nullable": true
                    },
                    "heldForYou": {
                      "type": "boolean"
                    },
                    "heldSeats": {
                      "type": "integer"
                    },
                    "heldTickets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/calendar/{token}.ics": {
      "get": {
        "tags": [
          "Calendar"
        ],
        "summary": "A private calendar feed",
        "description": "A subscribable feed (#403) by its secret token: a customer's bookings, or a staff member's sessions (`scope=all`: the whole schedule, for staff who may read every session). Whoever has the URL sees it; the owner revokes it by making a new one. 404 for an unknown or revoked token.",
        "operationId": "publicPrivateCalendarFeed",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "token",
            "in": "path",
            "description": "48 hex characters",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{48}$"
            }
          },
          {
            "name": "scope",
            "in": "query",
            "description": "all: the whole schedule (staff feeds only)",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "all"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "iCalendar feed (ETag; 304 when unchanged)",
            "content": {
              "text/calendar": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (If-None-Match)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/calendar.ics": {
      "get": {
        "tags": [
          "Calendar"
        ],
        "summary": "The public schedule as a calendar feed",
        "description": "The organisation's public schedule (#403) as a subscribable feed, optionally of one published activity or one location. 404 for an unknown organisation, activity or location.",
        "operationId": "publicScheduleCalendar",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "activityId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "iCalendar feed (ETag; 304 when unchanged)",
            "content": {
              "text/calendar": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (If-None-Match)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/cancellation-policy": {
      "get": {
        "tags": [
          "Booking form"
        ],
        "summary": "The cancellation policy that applies",
        "description": "The cancellation policy of a session or series (its own, else the activity's, else the organisation's, #391), or the organisation's without either id. A moment of a service booked at a free moment (#935), before it is booked: `slotOfferId` + `start` (the activity's policy, else the organisation's). With a session, series or moment, `freeUntil` is the moment free cancellation ends (#700). An id of another organisation falls back to the organisation's policy. 400 for a malformed id.",
        "operationId": "publicGetCancellationPolicy",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "sessionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "seriesId",
            "in": "query",
            "description": "Used when no sessionId is given",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "slotOfferId",
            "in": "query",
            "description": "A moment of this slot offer (#935), with start; used when neither sessionId nor seriesId is given",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "start",
            "in": "query",
            "description": "The moment's start, ISO 8601 with offset (with slotOfferId)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The policy",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "policy": {
                      "$ref": "#/components/schemas/SeriesSettingsCancellationPolicy"
                    },
                    "currency": {
                      "description": "The currency fixed fees are charged in: the session's or series' price currency, else the organisation's",
                      "type": "string"
                    },
                    "freeUntil": {
                      "description": "Free cancellation ends then; null without a session/series or without a free window",
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/checkout/quote": {
      "post": {
        "tags": [
          "Checkout"
        ],
        "summary": "What a checkout will cost",
        "description": "Per participant or party, with make-up credits and one discount code. A moment of a service booked at a free moment (#924) is quoted with `slot` instead of sessionId/seriesId: 409 {code: slot_taken} when it is no longer free. `participants[].entitlementId` (signed in) quotes a seat paid with a pass or subscription, also for a moment (#935).",
        "operationId": "quoteCheckout",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "seriesId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "slot": {
                    "$ref": "#/components/schemas/CheckoutSlot"
                  },
                  "participants": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "tickets": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "discountCode": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The quote",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "total": {
                      "type": "object"
                    },
                    "lines": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "409": {
            "description": "slot_taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/checkout/customer": {
      "post": {
        "tags": [
          "Checkout"
        ],
        "summary": "Booking step 1: the booker",
        "description": "Account-first booking (#422, #423): the booker's details before choosing participants. A new address creates an unverified customer and answers 201 with a `checkoutToken` (send it to /checkout, /waitlist) and no email yet: the review step asks for a code with POST /auth/code purpose `verify`. A known address answers 200 `existing` and emails a sign-in code; nothing about that account is shown or changed. With the `checkoutToken` of an account made earlier in this flow and still unverified, that account is updated (e.g. a corrected email) instead. Phone is required unless the organisation's booking rules make it optional; `customFields` are the organisation's customer fields (422 `details` `customFields.{name}`). Rate-limited per IP address and per address.",
        "operationId": "publicCheckoutCustomer",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email",
                  "firstName",
                  "lastName"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "firstName": {
                    "type": "string"
                  },
                  "lastName": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "customFields": {
                    "description": "Customer custom field name => value",
                    "type": "object"
                  },
                  "locale": {
                    "description": "Language of the sign-in code email",
                    "type": "string"
                  },
                  "checkoutToken": {
                    "description": "From an earlier answer of this endpoint",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`created` (own account updated) or `existing` (a sign-in code was emailed)",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "created",
                        "existing"
                      ]
                    },
                    "maskedEmail": {
                      "type": "string",
                      "example": "m•••@example.com"
                    },
                    "checkoutToken": {
                      "description": "Only when created",
                      "type": "string"
                    },
                    "resendAfter": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "A new unverified account",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "created"
                    },
                    "maskedEmail": {
                      "type": "string"
                    },
                    "checkoutToken": {
                      "type": "string"
                    },
                    "resendAfter": {
                      "type": "integer",
                      "example": 0
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Validation failed: `details` per field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts (`rate_limited`, `retryAfter`, `Retry-After` header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/checkout": {
      "post": {
        "tags": [
          "Checkout"
        ],
        "summary": "Book",
        "description": "Books one or more participants (or a party) at a session, a series or, with `slot` (#924), a moment of a service booked at a free moment; account-first: an unknown customer gets an account. A moment: 409 {code: slot_taken | too_many_holds | not_available | appointment_only}, 422 {code: not_provider | invalid_slot}, 404 {code: unknown_offer}. A saved participant's `participants[].entitlementId` pays their seat with a pass, subscription or make-up credit covering the activity, a moment too (#935; GET /customer/{org}/entitlements?slotOfferId=&start= lists them): free and confirmed. Payment then goes through POST /bookings/{id}/payment.",
        "operationId": "placeCheckout",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "seriesId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "slot": {
                    "$ref": "#/components/schemas/CheckoutSlot"
                  },
                  "participants": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "tickets": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "customer": {
                    "type": "object"
                  },
                  "termsAccepted": {
                    "type": "boolean"
                  },
                  "discountCode": {
                    "type": "string"
                  },
                  "giftCardCodes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booked",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "checkoutId": {
                      "type": "string"
                    },
                    "bookings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "No longer available",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Invalid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/waitlist": {
      "post": {
        "tags": [
          "Waitlist"
        ],
        "summary": "Join the waiting list",
        "description": "Joins the waiting list of a full session or series (#502), account-first like the checkout: signed in (optional `Authorization: Bearer` customer token; 401 `unauthenticated` when it is invalid), the account from /checkout/customer (`checkoutToken`), or a `customer` contact. Saved participants need a signed-in verified customer (403 `login_required` / `email_not_verified`). A headcount activity's session takes a party: `tickets` instead of `participants` (#796; participants are refused with 422 `headcount_only`). Emails a \"you are on the waiting list\" confirmation with the places. 409 `not_full` while a seat (or room for the party) is free, 409 `already_waiting` (with the `place` already held) / `already_booked` with `participantIndex`, 409 `not_available` when not on offer online, 404 `not_found`. Rate-limited per IP address.",
        "operationId": "publicJoinWaitlist",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "sessionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "seriesId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "participants": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/PublicWaitlistParticipantInput"
                    }
                  },
                  "tickets": {
                    "description": "A party (sessions of a headcount activity only)",
                    "type": "array",
                    "items": {
                      "properties": {
                        "ticketTypeId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "quantity": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "checkoutToken": {
                    "type": "string"
                  },
                  "customer": {
                    "$ref": "#/components/schemas/PublicCheckoutContactInput"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "On the waiting list",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "entries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicWaitlistJoinedEntry"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "`unauthenticated`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "403": {
            "description": "`login_required` or `email_not_verified`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "409": {
            "description": "`not_full`, `already_waiting` (+ `place`), `already_booked` (+ `participantIndex`), `not_available`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed or a ticket/participant refused (`details`, `code`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts (`rate_limited`, `retryAfter`, `Retry-After` header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/interest": {
      "post": {
        "tags": [
          "Waitlist"
        ],
        "summary": "Notify me when I can book",
        "description": "Registers a \"notify me when I can book\" request (#511) for exactly one of an activity (once it has bookable dates), a session or a series (once its booking window opens). Signed in (optional `Authorization: Bearer` customer token; 401 `unauthenticated` when invalid) or with a `customer` contact that creates an unverified account or attaches to the existing one without showing or changing it; the answer is the same either way. 409 `already_bookable` when it can be booked now, 404 `not_found`, 422 for missing fields. Rate-limited per IP address and per address.",
        "operationId": "publicRegisterInterest",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "activityId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "sessionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "seriesId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "customer": {
                    "$ref": "#/components/schemas/PublicCheckoutContactInput"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "registered"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "`unauthenticated`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "409": {
            "description": "`already_bookable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed: `details` per field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts (`rate_limited`, `retryAfter`, `Retry-After` header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/email-check": {
      "get": {
        "tags": [
          "Checkout"
        ],
        "summary": "Can this address receive email?",
        "description": "Checks the domain of an address typed in the booking form (#342: catches typos like gmial.com). Fails open: `deliverable` is null when DNS cannot tell. 422 for something that is no address. Rate-limited per IP address.",
        "operationId": "publicEmailCheck",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "email",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "email"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The domain check",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "domain": {
                      "type": "string"
                    },
                    "deliverable": {
                      "type": "boolean",
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Not an email address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts (`rate_limited`, `retryAfter`, `Retry-After` header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/code": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Email a sign-in code",
        "description": "Emails a 6-digit code and a sign-in link to the address. Answers 202 for every valid request, whether or not the address has an account at this organisation, so it never tells who is a customer. Purposes: `login` (default), `verify` (confirm the address of an account made by the booking form), `password_reset` (then POST /auth/password/reset). Rate-limited per IP address and per address, one code per 30 seconds per address. 422 for an invalid address or unknown purpose.",
        "operationId": "customerRequestSignInCode",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "purpose": {
                    "type": "string",
                    "default": "login",
                    "enum": [
                      "login",
                      "verify",
                      "password_reset"
                    ]
                  },
                  "locale": {
                    "description": "Language of the email (en, nl, fr, de); default: the request language",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Sent when the address has an account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthCodeSent"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid email address or purpose",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/code/verify": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Sign in with an emailed code",
        "description": "Redeems the 6-digit code from POST /auth/code, POST /auth/register or the booking form and starts a session. Redeeming a code verifies the email address. 400 `invalid_code` for a wrong, used or expired code. Rate-limited per IP address and per address.",
        "operationId": "customerSignInWithCode",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email",
                  "code"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string",
                    "example": "123456"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthSession"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_code`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/link": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Sign in with an emailed link",
        "description": "Redeems the token of an emailed sign-in link (magic link, \"Manage your booking\" in the confirmation email) and starts a session; verifies the email address. 400 `invalid_link` when it is invalid or expired. Rate-limited per IP address.",
        "operationId": "customerSignInWithLink",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "description": "The token from the emailed link",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthSession"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_link`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/login": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Sign in with a password",
        "description": "For customers who chose a password (it is optional). 401 for a wrong address or password. Rate-limited per IP address and per address.",
        "operationId": "customerSignInWithPassword",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthSession"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/register": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Create an account",
        "description": "Creates a customer account without booking and emails a code to confirm the address (an existing account gets a sign-in code instead). Answers 202 either way, so it never tells who is a customer; then sign in with POST /auth/code/verify. 403 `registration_closed` when the organisation only takes invited customers, 422 for invalid details. Rate-limited per IP address.",
        "operationId": "customerRegister",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email",
                  "firstName",
                  "lastName"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "firstName": {
                    "type": "string"
                  },
                  "lastName": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true
                  },
                  "locale": {
                    "description": "Language of the email (en, nl, fr, de); default: the request language",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Check your inbox",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthCodeSent"
                }
              }
            }
          },
          "403": {
            "description": "`registration_closed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/password/reset": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Choose a new password with an emailed code",
        "description": "Takes a code requested with POST /auth/code purpose `password_reset`, sets the new password and starts a session. 400 `invalid_code`, 422 for an unacceptable password. Rate-limited per IP address and per address.",
        "operationId": "customerResetPassword",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "email",
                  "code",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthSession"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_code`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid password",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/refresh": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Renew the customer tokens",
        "description": "Exchanges a refresh token for a new access and refresh token; the old refresh token stops working. 400 without a refresh token, 401 when it is invalid or expired.",
        "operationId": "customerRefreshToken",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "refreshToken"
                ],
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New tokens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthTokens"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/logout": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Sign out",
        "description": "Ends the session of the refresh token. Always 204; an access token already handed out stays valid until it expires. Signing out everywhere: POST /api/customer/{org_slug}/logout-all.",
        "operationId": "customerSignOut",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "refreshToken": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Signed out"
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/oidc/providers": {
      "get": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "External sign-in providers",
        "description": "The external sign-in providers (Google, Microsoft) configured on this Plek installation, so a page only shows buttons that work.",
        "operationId": "customerListSignInProviders",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Providers",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "providers": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "id": {
                            "type": "string",
                            "example": "google"
                          },
                          "name": {
                            "type": "string",
                            "example": "Google"
                          }
                        },
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/oidc/{provider}/start": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Start signing in with Google or Microsoft",
        "description": "Step 1 of external sign-in: the page keeps a random verifier and sends its SHA-256 (`clientChallenge`) and its own URL (`returnUrl`, an allowed website of the organisation, its booking site or the customer app). Send the browser to `authorizationUrl`; the provider returns to the API, which sends the browser back to `returnUrl` with `#plek_sso=<ticket>` (or `#plek_sso_error=<reason>`). Then POST /auth/oidc/redeem. 404 for a provider that is not configured, 422 for a return URL that is not allowed or a malformed challenge, 502 `external_sign_in_failed`. Rate-limited per IP address.",
        "operationId": "customerStartExternalSignIn",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "example": "google"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "returnUrl",
                  "clientChallenge"
                ],
                "properties": {
                  "returnUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "clientChallenge": {
                    "description": "SHA-256 of the page verifier (PKCE S256)",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Where to send the browser",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "authorizationUrl": {
                      "type": "string",
                      "format": "uri"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Return URL not allowed or malformed challenge",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          },
          "502": {
            "description": "`external_sign_in_failed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/auth/oidc/redeem": {
      "post": {
        "tags": [
          "Customer sign-in"
        ],
        "summary": "Finish signing in with Google or Microsoft",
        "description": "Step 2 of external sign-in: redeems the ticket from `#plek_sso=` with the verifier kept by the page and starts a session (finding or creating the customer by the verified address of the provider). 400 `external_sign_in_failed` for an invalid, used or expired ticket or a wrong verifier. Rate-limited per IP address.",
        "operationId": "customerRedeemExternalSignIn",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "ticket",
                  "verifier"
                ],
                "properties": {
                  "ticket": {
                    "type": "string"
                  },
                  "verifier": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicAuthSession"
                }
              }
            }
          },
          "400": {
            "description": "`external_sign_in_failed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/PublicAuthTooManyAttempts"
          }
        }
      }
    },
    "/api/public/{org_slug}/push/unsubscribe": {
      "post": {
        "tags": [
          "Preferences"
        ],
        "summary": "A device stops push notifications",
        "description": "Called by the customer app when signing out (#356): removes the push subscription with this endpoint, without authentication. 204 whether or not it existed; 404 without an endpoint or for an unknown organisation.",
        "operationId": "publicUnsubscribePush",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "endpoint"
                ],
                "properties": {
                  "endpoint": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Removed (or never there)"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/payment-options": {
      "get": {
        "tags": [
          "Booking form"
        ],
        "summary": "Ways to pay the booking flow offers",
        "description": "Online (an active payment gateway), on site and by bank transfer (#397; bank transfer only with an IBAN set).",
        "operationId": "publicGetPaymentOptions",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "The options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicFormPaymentOptions"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/pay-offline": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Pay later, on site or by bank transfer",
        "description": "Instead of paying online, the customer pays on site or by bank transfer (#397): confirms the booking and the other bookings of its checkout, with a payment reference and deadline. 400 when the method is unknown or not offered by the organisation, or the booking no longer awaits payment; 404 for an unknown booking.",
        "operationId": "publicPayBookingOffline",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "A booking of the checkout",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "method"
                ],
                "properties": {
                  "method": {
                    "type": "string",
                    "enum": [
                      "on_site",
                      "bank_transfer"
                    ]
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed, with how to pay",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "bookings": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "status": {
                            "type": "string"
                          }
                        },
                        "type": "object"
                      }
                    },
                    "payment": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PublicFormOfflinePayment"
                        }
                      ],
                      "nullable": true
                    },
                    "amount": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PublicFormAmount"
                        }
                      ],
                      "nullable": true,
                      "description": "Total still to pay for the checkout"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/activities": {
      "get": {
        "tags": [
          "Activities"
        ],
        "summary": "List published activities",
        "description": "Retrieve all published activities for an organisation. Only activities marked as published will be returned.",
        "operationId": "listPublicActivities",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "List of activities",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "activities": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicActivity"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/activities/{id}": {
      "get": {
        "tags": [
          "Activities"
        ],
        "summary": "Get activity details",
        "description": "Retrieve details of a specific published activity.",
        "operationId": "getPublicActivity",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Activity details",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "activity": {
                      "$ref": "#/components/schemas/PublicActivity"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/session": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Create a session booking",
        "description": "Create a booking for a single session. This endpoint handles guest checkout by creating/finding customer and participant records automatically.",
        "operationId": "createPublicSessionBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "sessionId",
                  "customer",
                  "participant"
                ],
                "properties": {
                  "sessionId": {
                    "description": "Session to book",
                    "type": "string",
                    "format": "uuid"
                  },
                  "customer": {
                    "required": [
                      "email",
                      "firstName",
                      "lastName"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string",
                        "nullable": true
                      },
                      "nationalRegistryNumber": {
                        "type": "string",
                        "nullable": true
                      }
                    },
                    "type": "object"
                  },
                  "participant": {
                    "required": [
                      "firstName",
                      "lastName"
                    ],
                    "properties": {
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "nationalRegistryNumber": {
                        "type": "string",
                        "nullable": true
                      },
                      "tshirtSize": {
                        "type": "string",
                        "nullable": true
                      },
                      "remarks": {
                        "type": "string",
                        "nullable": true
                      }
                    },
                    "type": "object"
                  },
                  "termsAccepted": {
                    "description": "Whether terms were accepted",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booking created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "booking": {
                      "$ref": "#/components/schemas/PublicBooking"
                    },
                    "customer": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string"
                        },
                        "fullName": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "participant": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "fullName": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/series": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Create a series booking",
        "description": "Create a booking for an entire series. This endpoint handles guest checkout by creating/finding customer and participant records automatically.",
        "operationId": "createPublicSeriesBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "seriesId",
                  "customer",
                  "participant"
                ],
                "properties": {
                  "seriesId": {
                    "description": "Series to book",
                    "type": "string",
                    "format": "uuid"
                  },
                  "customer": {
                    "required": [
                      "email",
                      "firstName",
                      "lastName"
                    ],
                    "properties": {
                      "email": {
                        "type": "string",
                        "format": "email"
                      },
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string",
                        "nullable": true
                      },
                      "nationalRegistryNumber": {
                        "type": "string",
                        "nullable": true
                      }
                    },
                    "type": "object"
                  },
                  "participant": {
                    "required": [
                      "firstName",
                      "lastName"
                    ],
                    "properties": {
                      "firstName": {
                        "type": "string"
                      },
                      "lastName": {
                        "type": "string"
                      },
                      "nationalRegistryNumber": {
                        "type": "string",
                        "nullable": true
                      },
                      "tshirtSize": {
                        "type": "string",
                        "nullable": true
                      },
                      "remarks": {
                        "type": "string",
                        "nullable": true
                      }
                    },
                    "type": "object"
                  },
                  "discountCode": {
                    "description": "Optional discount code",
                    "type": "string",
                    "nullable": true
                  },
                  "termsAccepted": {
                    "description": "Whether terms were accepted",
                    "type": "boolean"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booking created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "booking": {
                      "$ref": "#/components/schemas/PublicBooking"
                    },
                    "customer": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "email": {
                          "type": "string"
                        },
                        "fullName": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "participant": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "fullName": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}": {
      "get": {
        "tags": [
          "Bookings"
        ],
        "summary": "Get booking details",
        "description": "Retrieve details of a booking including payment status and applied gift cards.",
        "operationId": "getPublicBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Booking details",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "booking": {
                      "$ref": "#/components/schemas/PublicBooking"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "tags": [
          "Bookings"
        ],
        "summary": "Abandon a pending booking",
        "description": "Cancel an unpaid (pending) booking and release the seat it holds. Confirmed bookings cannot be cancelled through this public endpoint (403); customers cancel them from their account.",
        "operationId": "cancelPublicBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Booking cancelled",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string",
                      "example": "Booking cancelled successfully"
                    },
                    "refund": {
                      "description": "Always false: only unpaid bookings can be cancelled here",
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "description": "Booking is confirmed; cancel it from the customer account"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/payment": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Initiate payment",
        "description": "Start the payment process for a booking. Returns a checkout URL to redirect the user to the payment gateway.",
        "operationId": "initiatePublicPayment",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "redirectUrl"
                ],
                "properties": {
                  "redirectUrl": {
                    "description": "URL to redirect after payment",
                    "type": "string",
                    "format": "uri",
                    "example": "https://example.com/booking/confirmation"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payment initiated",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "payment": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string"
                        },
                        "amount": {
                          "$ref": "#/components/schemas/Money"
                        },
                        "checkoutUrl": {
                          "description": "Redirect user here to complete payment",
                          "type": "string",
                          "format": "uri"
                        },
                        "gatewayTransactionId": {
                          "type": "string"
                        },
                        "gatewayType": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/gift-cards": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Apply gift cards",
        "description": "Apply one or more gift cards to a booking to reduce the payment amount.",
        "operationId": "applyGiftCardsToBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "giftCardCodes"
                ],
                "properties": {
                  "giftCardCodes": {
                    "description": "Gift card codes to apply",
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "TT-XXXX-XXXX",
                      "TT-YYYY-YYYY"
                    ]
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Gift cards applied",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "appliedGiftCards": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "giftCardId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "code": {
                            "type": "string"
                          },
                          "amountRedeemed": {
                            "$ref": "#/components/schemas/Money"
                          },
                          "remainingBalance": {
                            "$ref": "#/components/schemas/Money"
                          }
                        },
                        "type": "object"
                      }
                    },
                    "totalGiftCardAmount": {
                      "$ref": "#/components/schemas/Money"
                    },
                    "remainingToPay": {
                      "$ref": "#/components/schemas/Money"
                    },
                    "fullyCovered": {
                      "description": "If true, no payment needed",
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/gift-cards/{code}": {
      "delete": {
        "tags": [
          "Bookings"
        ],
        "summary": "Remove gift card",
        "description": "Remove a previously applied gift card from a booking.",
        "operationId": "removeGiftCardFromBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          },
          {
            "name": "code",
            "in": "path",
            "description": "Gift card code to remove",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Gift card removed",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "removed": {
                      "description": "The removed gift card code",
                      "type": "string"
                    },
                    "amountRestored": {
                      "$ref": "#/components/schemas/Money"
                    },
                    "newRemainingToPay": {
                      "$ref": "#/components/schemas/Money"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/confirm": {
      "post": {
        "tags": [
          "Bookings"
        ],
        "summary": "Confirm booking (gift card only)",
        "description": "Confirm a booking that is fully covered by gift cards. This endpoint only works when the booking has zero remaining payment due.",
        "operationId": "confirmPublicBooking",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Booking confirmed",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "booking": {
                      "$ref": "#/components/schemas/PublicBooking"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/calendar.ics": {
      "get": {
        "tags": [
          "Calendar"
        ],
        "summary": "Download calendar file",
        "description": "Download an ICS calendar file for a booking to import into calendar applications.",
        "operationId": "downloadBookingCalendar",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "ICS calendar file",
            "content": {
              "text/calendar": {}
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/bookings/{id}/calendar-links": {
      "get": {
        "tags": [
          "Calendar"
        ],
        "summary": "Get calendar links",
        "description": "Get URLs for adding a booking to various calendar applications (Google Calendar, ICS download).",
        "operationId": "getBookingCalendarLinks",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Calendar links",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "googleCalendarUrl": {
                      "description": "URL to add event to Google Calendar (null for series bookings)",
                      "type": "string",
                      "format": "uri",
                      "nullable": true
                    },
                    "icsDownloadUrl": {
                      "description": "Relative URL to download ICS file",
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/custom-fields": {
      "get": {
        "tags": [
          "Booking form"
        ],
        "summary": "Custom fields of the booking form",
        "description": "The organisation's custom fields asked for customers and participants in the booking form (#801), in order. Booking questions per session or series are GET /booking-questions.",
        "operationId": "publicListCustomFields",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Fields",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "customFieldDefinitions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicFormCustomField"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/booking-questions": {
      "get": {
        "tags": [
          "Booking form"
        ],
        "summary": "Booking questions and consents",
        "description": "The questions and consents asked when booking a session or series (#395), or when requesting a venue hire of an activity (#794). Answer them in the checkout's `answers` (once per booking) or `participants[].answers` (`perParticipant`). 400 without sessionId, seriesId or activityId.",
        "operationId": "publicListBookingQuestions",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "sessionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "seriesId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "activityId",
            "in": "query",
            "description": "A venue hire request",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Questions",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "questions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicFormBookingQuestion"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "No item given",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/discount-codes/validate": {
      "post": {
        "tags": [
          "Discount Codes"
        ],
        "summary": "Validate discount code",
        "description": "Validate a discount code and calculate the discount amount for a given order.",
        "operationId": "validatePublicDiscountCode",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "code",
                  "bookingType",
                  "orderAmountValue",
                  "orderAmountCurrency"
                ],
                "properties": {
                  "code": {
                    "description": "Discount code to validate",
                    "type": "string"
                  },
                  "bookingType": {
                    "description": "Type of booking",
                    "type": "string",
                    "enum": [
                      "session",
                      "series"
                    ]
                  },
                  "orderAmountValue": {
                    "description": "Order amount in smallest currency unit",
                    "type": "integer"
                  },
                  "orderAmountCurrency": {
                    "description": "Currency code",
                    "type": "string",
                    "example": "EUR"
                  },
                  "customerId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscountValidation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/gift-cards/options": {
      "get": {
        "tags": [
          "Gift Cards"
        ],
        "summary": "Get gift card purchase options",
        "description": "Retrieve available gift card denominations and configuration for an organisation.",
        "operationId": "getGiftCardOptions",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Gift card options",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "giftCardOptions": {
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "denominations": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Money"
                          }
                        },
                        "currency": {
                          "type": "string",
                          "example": "EUR"
                        },
                        "expiryMonths": {
                          "type": "integer",
                          "example": 12
                        },
                        "allowCustomAmount": {
                          "type": "boolean"
                        },
                        "minimumAmount": {
                          "type": "integer",
                          "nullable": true
                        },
                        "maximumAmount": {
                          "type": "integer",
                          "nullable": true
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/gift-cards/validate": {
      "post": {
        "tags": [
          "Gift Cards"
        ],
        "summary": "Validate gift card code",
        "description": "Validate a gift card code and retrieve its current balance.",
        "operationId": "validateGiftCard",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "description": "Gift card code",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string",
                      "nullable": true
                    },
                    "code": {
                      "description": "Why the code cannot be used (when valid is false)",
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "invalid_code",
                        "expired",
                        "depleted",
                        "not_active"
                      ]
                    },
                    "giftCard": {
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "currentBalance": {
                          "$ref": "#/components/schemas/Money"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "type": "object",
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/gift-cards/order": {
      "post": {
        "tags": [
          "Gift Cards"
        ],
        "summary": "Create gift card order",
        "description": "Create a gift card order and initiate payment. Returns a checkout URL to complete payment.",
        "operationId": "createGiftCardOrder",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "customerEmail",
                  "items",
                  "returnUrl"
                ],
                "properties": {
                  "customerEmail": {
                    "type": "string",
                    "format": "email"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "properties": {
                        "amount": {
                          "description": "Amount in smallest currency unit",
                          "type": "integer"
                        },
                        "recipientName": {
                          "type": "string"
                        },
                        "recipientEmail": {
                          "type": "string",
                          "format": "email"
                        },
                        "message": {
                          "type": "string",
                          "nullable": true
                        }
                      },
                      "type": "object"
                    }
                  },
                  "returnUrl": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GiftCardOrder"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/gift-cards/order/{order_id}": {
      "get": {
        "tags": [
          "Gift Cards"
        ],
        "summary": "Get gift card order status",
        "description": "Retrieve the status and details of a gift card order. Gift card codes are included after payment is complete.",
        "operationId": "getGiftCardOrder",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "order_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Order details",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "order": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "pending",
                            "paid",
                            "cancelled"
                          ]
                        },
                        "customerEmail": {
                          "type": "string",
                          "format": "email"
                        },
                        "totalAmount": {
                          "$ref": "#/components/schemas/Money"
                        },
                        "items": {
                          "type": "array",
                          "items": {
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "amount": {
                                "$ref": "#/components/schemas/Money"
                              },
                              "recipientName": {
                                "type": "string"
                              },
                              "recipientEmail": {
                                "type": "string",
                                "format": "email"
                              },
                              "message": {
                                "type": "string",
                                "nullable": true
                              },
                              "giftCard": {
                                "properties": {
                                  "code": {
                                    "type": "string"
                                  },
                                  "expiresAt": {
                                    "type": "string",
                                    "format": "date-time"
                                  }
                                },
                                "type": "object",
                                "nullable": true
                              }
                            },
                            "type": "object"
                          }
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/hire-requests/{id}/form": {
      "get": {
        "tags": [
          "Hire requests"
        ],
        "summary": "The venue hire request form",
        "description": "What the request form of a venue hire activity (a published headcount activity with booking style `request`, #794) asks: its locations with their day parts (hours per weekday) and extras, the ticket types, whether billing details and a phone number are required, the earliest date and the deposit rule. Texts in the request language. 404 `not_requestable` for any other activity.",
        "operationId": "publicHireRequestForm",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Activity id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The form",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "activity": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "nullable": true
                        }
                      },
                      "type": "object"
                    },
                    "headcount": {
                      "description": "Party rules: minimum/maximum seats, phone required",
                      "type": "object"
                    },
                    "ticketTypes": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "requiresBillingDetails": {
                      "type": "boolean"
                    },
                    "phoneRequired": {
                      "type": "boolean"
                    },
                    "locations": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "name": {
                            "type": "string"
                          },
                          "address": {
                            "type": "string"
                          },
                          "dayParts": {
                            "type": "array",
                            "items": {
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "name": {
                                  "type": "string"
                                },
                                "hours": {
                                  "description": "ISO weekday => {start, end} in local time",
                                  "type": "object"
                                }
                              },
                              "type": "object"
                            }
                          },
                          "extras": {
                            "type": "array",
                            "items": {
                              "type": "object"
                            }
                          }
                        },
                        "type": "object"
                      }
                    },
                    "timezone": {
                      "type": "string",
                      "example": "Europe/Brussels"
                    },
                    "earliestDate": {
                      "type": "string",
                      "format": "date"
                    },
                    "deposit": {
                      "description": "Deposit rule: percentage or fixed amount",
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "`not_requestable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/hire-requests/availability": {
      "get": {
        "tags": [
          "Hire requests"
        ],
        "summary": "Can this day part be requested on this date?",
        "description": "For the request form: the hours of a day part on a date and whether it can still be requested (`available` false with `code: too_soon` within the activity's notice). `otherRequestPending` warns that another open request overlaps at that location; it does not stop a request. 404 `not_requestable`, 422 `unknown_day_part` for a day part the activity does not offer, 422 `day_part_closed` when it is closed that day.",
        "operationId": "publicHireRequestAvailability",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "activityId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "dayPartId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Availability",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "code": {
                      "type": "string",
                      "example": "too_soon",
                      "nullable": true
                    },
                    "startsAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "endsAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "otherRequestPending": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "`not_requestable`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "422": {
            "description": "`unknown_day_part`, `day_part_closed`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/hire-requests": {
      "post": {
        "tags": [
          "Hire requests"
        ],
        "summary": "Request a venue hire",
        "description": "Sends a venue hire request (#794): no payment, staff answer with a quote the customer accepts in their account. Account-first like the checkout: signed in (optional `Authorization: Bearer` customer token; 401 `unauthenticated` when invalid) or a `customer` contact that creates an unverified account or attaches to the existing one without showing or changing it. Billing details are saved on the account only when it is the customer's own. 422 codes: `validation_failed`, `not_requestable`, `unknown_day_part`, `day_part_closed`, `outside_day_part`, `too_soon`, party ticket codes, `billing_details_required`, `invalid_billing_details`, `answers_required`. Rate-limited per IP address.",
        "operationId": "publicCreateHireRequest",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "activityId",
                  "dayPartId",
                  "date",
                  "tickets"
                ],
                "properties": {
                  "activityId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "dayPartId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "date": {
                    "type": "string",
                    "format": "date"
                  },
                  "startTime": {
                    "description": "Within the day part; default its start",
                    "type": "string",
                    "example": "18:00"
                  },
                  "endTime": {
                    "type": "string",
                    "example": "23:00"
                  },
                  "tickets": {
                    "type": "array",
                    "items": {
                      "properties": {
                        "ticketTypeId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "quantity": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "extras": {
                    "type": "array",
                    "items": {
                      "properties": {
                        "extraId": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "quantity": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "customer": {
                    "$ref": "#/components/schemas/PublicCheckoutContactInput"
                  },
                  "billingDetails": {
                    "description": "{companyName, vatNumber?, street, postcode, town, country}",
                    "type": "object"
                  },
                  "answers": {
                    "description": "Booking question name => answer",
                    "type": "object"
                  },
                  "remarks": {
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Requested",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "hire": {
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "reference": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "example": "new"
                        },
                        "date": {
                          "type": "string",
                          "format": "date"
                        },
                        "startsAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "endsAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "partySize": {
                          "type": "integer"
                        }
                      },
                      "type": "object"
                    },
                    "otherRequestPending": {
                      "type": "boolean"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "`unauthenticated`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`space_conflict`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "422": {
            "description": "Refused (see codes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          },
          "429": {
            "description": "Too many attempts (`rate_limited`, `retryAfter`, `Retry-After` header)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/locations": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "List locations",
        "description": "Retrieve all locations for an organisation.",
        "operationId": "listPublicLocations",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "List of locations",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "locations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicLocation"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/locations/{id}": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "Get location details",
        "description": "Retrieve details of a specific location.",
        "operationId": "getPublicLocation",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Location details",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "location": {
                      "$ref": "#/components/schemas/PublicLocation"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/manifest.webmanifest": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Web app manifest of the booking site",
        "description": "The installable booking site (#525, #356): name, theme colour from the branding and icons (maskable ones included); the app opens on the customer's upcoming sessions. Cached an hour.",
        "operationId": "publicWebManifest",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "The manifest",
            "content": {
              "application/manifest+json": {
                "schema": {
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "short_name": {
                      "type": "string"
                    },
                    "start_url": {
                      "type": "string"
                    },
                    "scope": {
                      "type": "string"
                    },
                    "display": {
                      "type": "string",
                      "example": "standalone"
                    },
                    "theme_color": {
                      "type": "string"
                    },
                    "background_color": {
                      "type": "string"
                    },
                    "icons": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/organisation": {
      "get": {
        "tags": [
          "Organisation"
        ],
        "summary": "Get organisation by slug",
        "description": "Retrieve public information about an organisation using its URL slug.",
        "operationId": "getPublicOrganisation",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Organisation details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicOrganisation"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/plans": {
      "get": {
        "tags": [
          "Plans"
        ],
        "summary": "Passes and subscriptions on sale",
        "description": "Plans that can be bought now (#377), texts in the request language. Buying needs a verified customer: POST /api/customer/{org_slug}/plan-purchases.",
        "operationId": "publicListPlans",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Plans",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "plans": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicPlan"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/plans/{id}": {
      "get": {
        "tags": [
          "Plans"
        ],
        "summary": "A pass or subscription on sale",
        "description": "One plan that can be bought now (#698: the detail page is public). 404 `not_on_sale` when it cannot be bought now.",
        "operationId": "publicGetPlan",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Plan id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicPlan"
                }
              }
            }
          },
          "404": {
            "description": "`not_on_sale`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/activities/{id}/reviews": {
      "get": {
        "tags": [
          "Reviews"
        ],
        "summary": "Reviews of an activity",
        "description": "Published reviews of the activity, newest first, with the average rating and count (#355). When the organisation doesn't show reviews publicly the answer is only `{\"shown\": false}`. Authors are shown as first name and initial.",
        "operationId": "publicListActivityReviews",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Activity id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 50,
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The reviews",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicReviewList"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/review-requests/opt-out": {
      "post": {
        "tags": [
          "Preferences"
        ],
        "summary": "Stop \"How was it?\" emails",
        "description": "The opt-out link of review request emails (#355): no more review requests for this customer. 404 when the customer id and token don't match.",
        "operationId": "publicOptOutOfReviewRequests",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "customerId",
                  "token"
                ],
                "properties": {
                  "customerId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "token": {
                    "description": "From the email's opt-out link",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Opted out"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/booking-nudges/opt-out": {
      "post": {
        "tags": [
          "Preferences"
        ],
        "summary": "Stop booking reminder emails",
        "description": "The opt-out link of \"you didn't finish your booking\" and \"spots left\" emails (#747): opts the customer out of these nudges. 404 when the customer id and token don't match.",
        "operationId": "publicOptOutOfBookingNudges",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "customerId",
                  "token"
                ],
                "properties": {
                  "customerId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "token": {
                    "description": "From the email's opt-out link",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Opted out"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/series": {
      "get": {
        "tags": [
          "Series"
        ],
        "summary": "List active series",
        "description": "Retrieve all active series for an organisation. Can be filtered by activity and/or location.",
        "operationId": "listPublicSeries",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "activityId",
            "in": "query",
            "description": "Filter by activity UUID",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "description": "Filter by location UUID",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of series",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "series": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicSeries"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/series/{id}": {
      "get": {
        "tags": [
          "Series"
        ],
        "summary": "One series",
        "description": "An active series of a published activity (#525), also when it cannot be booked right now, with the dates of its sessions still to come. 404 for an unknown series; 404 `not_available` when it is not on offer (#781).",
        "operationId": "publicGetSeries",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Series id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The series",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "series": {
                      "$ref": "#/components/schemas/PublicSeriesDetail"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or `not_available`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/sessions/{id}": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Get session details",
        "description": "Retrieve details of a specific session including availability.",
        "operationId": "getPublicSession",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "$ref": "#/components/parameters/ResourceId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session details",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "session": {
                      "$ref": "#/components/schemas/PublicSession"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/sessions": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "List upcoming sessions (agenda)",
        "description": "Upcoming sessions of all published activities within a date range (at most 62 days, never before now), sorted by start time. `nextStart` is the first matching session after the range, so an empty day can point to the next one.",
        "operationId": "listPublicSessions",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start of the range (ISO 8601); defaults to now",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "End of the range (ISO 8601); defaults to 14 days after `from`",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "age",
            "in": "query",
            "description": "The child's age in whole years; activities whose age range covers part of that year",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 100,
              "minimum": 0
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "activityId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Upcoming sessions",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicSession"
                      }
                    },
                    "dateRange": {
                      "properties": {
                        "start": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "type": "object"
                    },
                    "hasMore": {
                      "description": "More sessions match than were returned",
                      "type": "boolean"
                    },
                    "nextStart": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Invalid date or age"
          }
        }
      }
    },
    "/api/public/{org_slug}/locations/{location_id}/sessions": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "List sessions by location",
        "description": "Retrieve sessions for a specific location within a date range. Defaults to current week if dates not specified.",
        "operationId": "listPublicSessionsByLocation",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "location_id",
            "in": "path",
            "description": "Location UUID",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "start",
            "in": "query",
            "description": "Start date (ISO 8601 format)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2024-01-15T00:00:00+00:00"
          },
          {
            "name": "end",
            "in": "query",
            "description": "End date (ISO 8601 format)",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "example": "2024-01-21T23:59:59+00:00"
          }
        ],
        "responses": {
          "200": {
            "description": "List of sessions",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "sessions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicSession"
                      }
                    },
                    "dateRange": {
                      "properties": {
                        "start": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "end": {
                          "type": "string",
                          "format": "date-time"
                        }
                      },
                      "type": "object"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/activities/{id}/slot-offers": {
      "get": {
        "tags": [
          "Activities"
        ],
        "summary": "Where and with whom a service is booked at a free moment",
        "description": "For an activity with bookingStyle \"slots\" (#924): its active offers per location, with the length of a moment, the seats per moment, the price (null: the activity's default price; ticketPrices for a headcount activity) and, when a moment takes a staff member, the staff members a customer can pick (first names only; members who opted out are left out but still take moments for \"anyone\").",
        "operationId": "listPublicSlotOffers",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Offers",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "offers": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "locationId": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "locationName": {
                            "type": "string"
                          },
                          "durationMinutes": {
                            "type": "integer"
                          },
                          "capacity": {
                            "type": "integer"
                          },
                          "price": {
                            "oneOf": [
                              {
                                "$ref": "#/components/schemas/Money"
                              }
                            ],
                            "nullable": true
                          },
                          "ticketPrices": {
                            "type": "object"
                          },
                          "resource": {
                            "type": "string",
                            "enum": [
                              "staff",
                              "space"
                            ]
                          },
                          "staff": {
                            "type": "array",
                            "items": {
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "firstName": {
                                  "type": "string"
                                }
                              },
                              "type": "object"
                            }
                          },
                          "providers": {
                            "description": "How many staff members (or spaces) can take a moment, also those not showing their name; with one there is nobody to choose (#962)",
                            "type": "integer"
                          }
                        },
                        "type": "object"
                      }
                    },
                    "timezone": {
                      "type": "string"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/activities/{id}/slot-days": {
      "get": {
        "tags": [
          "Activities"
        ],
        "summary": "The days with free moments",
        "description": "Per local date (the organisation's timezone) the number of free moments, from `from` (default today) to `to` (default from + 30 days; at most 31 days per call). With staffId only that staff member's moments, else anyone's. locationId is needed when the service is offered at more than one location.",
        "operationId": "listPublicSlotDays",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "staffId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Days",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "offerId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "timezone": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string",
                      "format": "date"
                    },
                    "to": {
                      "type": "string",
                      "format": "date"
                    },
                    "days": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "date": {
                            "type": "string",
                            "format": "date"
                          },
                          "slots": {
                            "type": "integer"
                          },
                          "reason": {
                            "description": "Only on a day without free moments: why. A staff member's moments must lie within their availability and the location's opening hours (its day parts).",
                            "type": "string",
                            "enum": [
                              "past",
                              "too_far_ahead",
                              "closed",
                              "no_availability",
                              "outside_opening_hours",
                              "too_soon",
                              "fully_booked"
                            ]
                          }
                        },
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "location_required, not_provider or invalid_range"
          },
          "429": {
            "description": "rate_limited"
          }
        }
      }
    },
    "/api/public/{org_slug}/activities/{id}/slots": {
      "get": {
        "tags": [
          "Activities"
        ],
        "summary": "The free moments of a day",
        "description": "The moments that can be booked on a local date, with the seats left and (for a staff offer) the staff members free then who show their name. Book one with POST /checkout and `slot: {slotOfferId, start, staffId?}`, `start` exactly as returned here.",
        "operationId": "listPublicSlots",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "staffId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Moments",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "offerId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "date": {
                      "type": "string",
                      "format": "date"
                    },
                    "timezone": {
                      "type": "string"
                    },
                    "slots": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "start": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "end": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "seatsLeft": {
                            "type": "integer"
                          },
                          "staff": {
                            "type": "array",
                            "items": {
                              "properties": {
                                "id": {
                                  "type": "string",
                                  "format": "uuid"
                                },
                                "firstName": {
                                  "type": "string"
                                }
                              },
                              "type": "object"
                            }
                          }
                        },
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "location_required, not_provider or invalid_date"
          },
          "429": {
            "description": "rate_limited"
          }
        }
      }
    },
    "/api/public/{org_slug}/tags": {
      "get": {
        "tags": [
          "Tags"
        ],
        "summary": "List tags",
        "description": "Retrieve all tags for an organisation. Tags can be used to filter activities, sessions, or series.",
        "operationId": "listPublicTags",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "List of tags",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "tags": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PublicTag"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/{org_slug}/terms/current": {
      "get": {
        "tags": [
          "Terms"
        ],
        "summary": "Get current terms and conditions",
        "description": "Retrieve the current terms and conditions for an organisation. Returns whether terms acceptance is required and the terms content.",
        "operationId": "getCurrentPublicTerms",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Terms and conditions",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "termsRequired": {
                      "type": "boolean"
                    },
                    "displayMode": {
                      "type": "string",
                      "nullable": true
                    },
                    "externalUrl": {
                      "type": "string",
                      "format": "uri",
                      "nullable": true
                    },
                    "terms": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/TermsAndConditions"
                        }
                      ],
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/public/widget-presets/{id}": {
      "get": {
        "tags": [
          "Widgets"
        ],
        "summary": "A widget preset, as the embed loads it",
        "description": "What `/embed/v1/plek.js` reads for `<div data-plek-widget=\"wgt_…\">`. No authentication and no credentials: any website may read it (CORS `*`). Cached for a minute and revalidated with the ETag `\"{id}-{version}\"` (304 on If-None-Match). `host` is the page's https origin, recorded at most hourly so staff see where the widget is used.",
        "operationId": "publicGetWidgetPreset",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^wgt_[0-9A-Za-z]{8}$"
            }
          },
          {
            "name": "host",
            "in": "query",
            "description": "The embedding page's origin",
            "required": false,
            "schema": {
              "type": "string",
              "example": "https://www.example.com"
            }
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The preset",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "public, max-age=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "id": {
                      "type": "string",
                      "example": "wgt_a1B2c3D4"
                    },
                    "organisation": {
                      "properties": {
                        "slug": {
                          "type": "string"
                        }
                      },
                      "type": "object"
                    },
                    "flow": {
                      "$ref": "#/components/schemas/WidgetFlow"
                    },
                    "config": {
                      "$ref": "#/components/schemas/WidgetConfig"
                    },
                    "version": {
                      "type": "integer"
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "304": {
            "description": "Not modified"
          },
          "404": {
            "description": "Unknown or archived (code not_found)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StaffErrorWithCode"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/{org_slug}/unsubscribe": {
      "post": {
        "tags": [
          "Preferences"
        ],
        "summary": "Unsubscribe from marketing email",
        "description": "The unsubscribe link of marketing email (#612): withdraws the customer's marketing consent. The link carries the customer id and a signed token; 404 when they don't match.",
        "operationId": "publicUnsubscribeFromMarketing",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "customerId",
                  "token"
                ],
                "properties": {
                  "customerId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "token": {
                    "description": "From the email's unsubscribe link",
                    "type": "string"
                  }
                },
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Unsubscribed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/sessions/{id}/spots": {
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "The spot map of a session",
        "description": "For a session with assigned spots (#496): every spot of the room with its status (free, taken or blocked), never who sits there. `map` is null when the session has no spot layout. With `selection: customer` the customer picks a free spot (checkout `participants[].spotId`).",
        "operationId": "publicGetSessionSpots",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Session id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The map",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "map": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PublicSpotsMap"
                        }
                      ],
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/series/{id}/spots": {
      "get": {
        "tags": [
          "Series"
        ],
        "summary": "The spot map of a series",
        "description": "For a series with assigned spots (#496): a spot is free when it is free in every session still to come (status free or taken). A series booking gets the same spot in every session to come. `map` is null when the series has no spot layout.",
        "operationId": "publicGetSeriesSpots",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Series id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The map",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "map": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/PublicSpotsMap"
                        }
                      ],
                      "nullable": true
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/public/{org_slug}/series/{id}/trial": {
      "get": {
        "tags": [
          "Series"
        ],
        "summary": "Trial class of a series",
        "description": "The trial offer of a series (#402: one session to try at this price; book it at checkout with `trial: true`) and its sessions still to come. `trialOffer` null and no sessions without an offer. Seat counts only when the organisation shows spots left.",
        "operationId": "publicGetSeriesTrial",
        "parameters": [
          {
            "$ref": "#/components/parameters/OrgSlug"
          },
          {
            "name": "id",
            "in": "path",
            "description": "Series id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The offer",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "trialOffer": {
                      "properties": {
                        "price": {
                          "$ref": "#/components/schemas/PublicFormAmount"
                        }
                      },
                      "type": "object",
                      "nullable": true
                    },
                    "sessions": {
                      "type": "array",
                      "items": {
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "startTime": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "endTime": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "availableSpots": {
                            "type": "integer"
                          },
                          "isFull": {
                            "type": "boolean"
                          }
                        },
                        "type": "object"
                      }
                    }
                  },
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Address": {
        "description": "Physical address",
        "properties": {
          "street": {
            "type": "string",
            "example": "123 Main Street"
          },
          "city": {
            "type": "string",
            "example": "Amsterdam"
          },
          "postal_code": {
            "type": "string",
            "example": "1012 AB"
          },
          "country": {
            "type": "string",
            "example": "Netherlands"
          }
        },
        "type": "object"
      },
      "BookingParticipant": {
        "required": [
          "firstName",
          "lastName",
          "email"
        ],
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "nullable": true
          }
        },
        "type": "object"
      },
      "CheckoutSlot": {
        "description": "A moment of a service booked at a free moment (#924), instead of sessionId/seriesId",
        "required": [
          "slotOfferId",
          "start"
        ],
        "properties": {
          "slotOfferId": {
            "type": "string",
            "format": "uuid"
          },
          "start": {
            "description": "Exactly as GET /activities/{id}/slots returns it",
            "type": "string",
            "format": "date-time"
          },
          "staffId": {
            "description": "Left out: the first free staff member in provider order",
            "type": "string",
            "format": "uuid",
            "nullable": true
          }
        },
        "type": "object"
      },
      "CustomerErrorWithCode": {
        "properties": {
          "error": {
            "description": "Message for people",
            "type": "string"
          },
          "code": {
            "description": "Stable code for clients",
            "type": "string"
          }
        },
        "type": "object"
      },
      "DiscountValidation": {
        "required": [
          "valid"
        ],
        "properties": {
          "valid": {
            "type": "boolean"
          },
          "error": {
            "type": "string",
            "nullable": true
          },
          "discountCode": {
            "type": "object",
            "nullable": true
          },
          "discountAmount": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "nullable": true
          },
          "discountedPrice": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Money"
              }
            ],
            "nullable": true
          }
        },
        "type": "object"
      },
      "ErrorResponse": {
        "description": "Standard error response",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "description": "Error message",
            "type": "string",
            "example": "Resource not found"
          }
        },
        "type": "object"
      },
      "GiftCardOrder": {
        "required": [
          "orderId",
          "checkoutUrl"
        ],
        "properties": {
          "orderId": {
            "type": "string",
            "format": "uuid"
          },
          "checkoutUrl": {
            "type": "string",
            "format": "uri"
          }
        },
        "type": "object"
      },
      "Image": {
        "required": [
          "id",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "alt": {
            "type": "string",
            "nullable": true
          },
          "position": {
            "type": "integer",
            "example": 0
          }
        },
        "type": "object"
      },
      "Money": {
        "description": "Monetary value representation",
        "required": [
          "amount",
          "currency",
          "formatted"
        ],
        "properties": {
          "amount": {
            "description": "Amount in smallest currency unit (e.g., cents)",
            "type": "integer",
            "example": 2500
          },
          "currency": {
            "description": "ISO 4217 currency code",
            "type": "string",
            "example": "EUR"
          },
          "formatted": {
            "description": "Human-readable formatted amount",
            "type": "string",
            "example": "€25.00"
          }
        },
        "type": "object"
      },
      "PublicActivity": {
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Morning Yoga"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "defaultPrice": {
            "$ref": "#/components/schemas/Money"
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Image"
            }
          }
        },
        "type": "object"
      },
      "PublicAuthCodeSent": {
        "properties": {
          "message": {
            "type": "string"
          },
          "resendAfter": {
            "description": "Seconds before another code can be sent to this address",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PublicAuthCustomer": {
        "description": "The signed-in customer",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "language": {
            "type": "string",
            "nullable": true
          },
          "emailVerified": {
            "description": "Signing in with a code, link or Google/Microsoft verifies the address",
            "type": "boolean"
          },
          "hasPassword": {
            "type": "boolean"
          },
          "marketingOptIn": {
            "type": "boolean"
          },
          "smsOptIn": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "PublicAuthSession": {
        "description": "A new customer session",
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/PublicAuthTokens"
          },
          {
            "properties": {
              "customer": {
                "$ref": "#/components/schemas/PublicAuthCustomer"
              }
            },
            "type": "object"
          }
        ]
      },
      "PublicAuthTokens": {
        "properties": {
          "accessToken": {
            "description": "Send as `Authorization: Bearer ...` on /api/customer/{org_slug} routes",
            "type": "string"
          },
          "tokenType": {
            "type": "string",
            "example": "Bearer"
          },
          "expiresIn": {
            "description": "Seconds the access token stays valid",
            "type": "integer"
          },
          "refreshToken": {
            "description": "Single use: /auth/refresh answers a new pair",
            "type": "string"
          },
          "refreshExpiresIn": {
            "description": "Seconds the refresh token stays valid",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PublicBooking": {
        "required": [
          "id",
          "status",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string"
          },
          "totalAmount": {
            "$ref": "#/components/schemas/Money"
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BookingParticipant"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "type": "object"
      },
      "PublicCheckoutContactInput": {
        "description": "The booker's contact when not signed in and without a checkoutToken: creates an unverified account, or attaches to the existing account of that address without showing or changing it (account-first, #427)",
        "required": [
          "email",
          "firstName",
          "lastName"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "phone": {
            "type": "string",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicFormAmount": {
        "properties": {
          "amount": {
            "description": "Minor units (cents)",
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          }
        },
        "type": "object"
      },
      "PublicFormBookingQuestion": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/PublicFormCustomField"
          },
          {
            "properties": {
              "description": {
                "description": "E.g. the text of a consent",
                "type": "string",
                "nullable": true
              },
              "options": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "nullable": true
              },
              "perParticipant": {
                "description": "Asked per participant, else once per booking",
                "type": "boolean"
              },
              "version": {
                "type": "integer"
              }
            },
            "type": "object"
          }
        ]
      },
      "PublicFormBookingWindow": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "open",
              "priority",
              "not_yet_open",
              "closed"
            ]
          },
          "opensAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "priorityOpensAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "closesAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicFormCustomField": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "entityType": {
            "description": "What it is asked for, e.g. customer or participant",
            "type": "string"
          },
          "fieldName": {
            "description": "Key to send the answer under",
            "type": "string"
          },
          "fieldType": {
            "description": "e.g. text, textarea, number, date, select, checkbox, phone",
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "isRequired": {
            "type": "boolean"
          },
          "sortOrder": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PublicFormMoney": {
        "properties": {
          "amount": {
            "description": "Minor units (cents)",
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "example": "EUR"
          },
          "formatted": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "PublicFormOfflinePayment": {
        "description": "How to pay later",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "on_site",
              "bank_transfer"
            ]
          },
          "reference": {
            "description": "Payment reference to mention",
            "type": "string",
            "nullable": true
          },
          "dueAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "iban": {
            "description": "Bank transfer only",
            "type": "string",
            "nullable": true
          },
          "bic": {
            "type": "string",
            "nullable": true
          },
          "accountHolder": {
            "type": "string",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicFormPaymentOptions": {
        "properties": {
          "online": {
            "description": "An active payment gateway",
            "type": "boolean"
          },
          "onSite": {
            "type": "boolean"
          },
          "bankTransfer": {
            "description": "Only with an IBAN set",
            "type": "boolean"
          },
          "paymentDeadlineDays": {
            "type": "integer",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicLocation": {
        "required": [
          "id",
          "name"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Main Studio"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Address"
              }
            ],
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicOrganisation": {
        "required": [
          "id",
          "name",
          "slug"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Yoga Studio Amsterdam"
          },
          "slug": {
            "type": "string",
            "example": "yoga-studio-amsterdam"
          },
          "timezone": {
            "description": "IANA timezone sessions happen in; the booking site shows days and times in it (#882)",
            "type": "string",
            "example": "Europe/Brussels"
          },
          "settings": {
            "properties": {
              "default_language": {
                "type": "string",
                "example": "nl"
              }
            },
            "type": "object"
          },
          "branding": {
            "description": "Colours, logo, favicon, app icon and sign-in background of the booking page",
            "properties": {
              "primary_color": {
                "type": "string",
                "example": "#1976d2",
                "nullable": true
              },
              "primary_light_color": {
                "type": "string",
                "example": "#e3f2fd",
                "nullable": true
              },
              "accent_color": {
                "type": "string",
                "example": "#ff4081",
                "nullable": true
              },
              "logo_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "favicon_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "app_icon_url": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "background_url": {
                "description": "Full-page WebP background of the sign-in screens (#644)",
                "type": "string",
                "format": "uri",
                "nullable": true
              }
            },
            "type": "object"
          },
          "filters": {
            "description": "Which browsing filters make sense (#842)",
            "properties": {
              "age": {
                "description": "A published activity has an age range (headcount activities never count)",
                "type": "boolean",
                "example": true
              }
            },
            "type": "object"
          },
          "checkout": {
            "description": "What the booking form asks for",
            "properties": {
              "phoneRequired": {
                "type": "boolean",
                "example": true
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "PublicPlan": {
        "description": "A pass or subscription on sale (#375, #376); no sale counts or tax details",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "subscription",
              "pass"
            ]
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "$ref": "#/components/schemas/PublicFormAmount"
          },
          "services": {
            "type": "array",
            "items": {
              "properties": {
                "activityId": {
                  "type": "string",
                  "format": "uuid"
                },
                "rule": {
                  "description": "unlimited, limited(count, per week|month|validity) or credits(cost)",
                  "type": "object"
                },
                "activityName": {
                  "type": "string",
                  "nullable": true
                }
              },
              "type": "object"
            }
          },
          "credits": {
            "description": "Credits of a pass",
            "type": "integer",
            "nullable": true
          },
          "duration": {
            "properties": {
              "count": {
                "type": "integer",
                "nullable": true
              },
              "unit": {
                "type": "string",
                "nullable": true
              },
              "endsAt": {
                "description": "A fixed end date",
                "type": "string",
                "format": "date",
                "nullable": true
              }
            },
            "type": "object"
          },
          "fixedStartDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "availableUntil": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "validFrom": {
            "description": "Bought today: valid from",
            "type": "string",
            "format": "date"
          },
          "validUntil": {
            "description": "Bought today: valid until",
            "type": "string",
            "format": "date"
          },
          "remaining": {
            "description": "Left to sell; null without a maximum",
            "type": "integer",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicReviewList": {
        "description": "`shown` false (and nothing else) when the organisation keeps reviews private",
        "properties": {
          "shown": {
            "type": "boolean"
          },
          "average": {
            "description": "Average rating (1-5) of the published reviews",
            "type": "number",
            "nullable": true
          },
          "count": {
            "type": "integer"
          },
          "reviews": {
            "type": "array",
            "items": {
              "properties": {
                "id": {
                  "type": "string"
                },
                "rating": {
                  "type": "integer",
                  "maximum": 5,
                  "minimum": 1
                },
                "comment": {
                  "type": "string",
                  "nullable": true
                },
                "author": {
                  "description": "First name and initial",
                  "type": "string"
                },
                "reply": {
                  "description": "The organisation's reply",
                  "type": "string",
                  "nullable": true
                },
                "repliedAt": {
                  "type": "string",
                  "format": "date-time",
                  "nullable": true
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "type": "object"
            }
          }
        },
        "type": "object"
      },
      "PublicSeries": {
        "required": [
          "id",
          "name",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "price": {
            "$ref": "#/components/schemas/Money"
          },
          "capacity": {
            "type": "integer"
          },
          "spotsAvailable": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "sessions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicSession"
            }
          }
        },
        "type": "object"
      },
      "PublicSeriesDetail": {
        "description": "A series on offer. The seat counts (availableSpots, bookedCount, sessionsWithCapacity, requiredSessionsWithCapacity) are left out unless the organisation shows spots left.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "activityId": {
            "type": "string",
            "format": "uuid"
          },
          "activityName": {
            "type": "string"
          },
          "requiresBillingDetails": {
            "type": "boolean"
          },
          "locationId": {
            "type": "string",
            "format": "uuid"
          },
          "locationName": {
            "type": "string"
          },
          "seriesPrice": {
            "$ref": "#/components/schemas/PublicFormMoney"
          },
          "administrativePrice": {
            "$ref": "#/components/schemas/PublicFormMoney"
          },
          "capacity": {
            "type": "integer"
          },
          "spotSelection": {
            "type": "string",
            "nullable": true,
            "enum": [
              "auto",
              "customer"
            ]
          },
          "freelyBookable": {
            "description": "Its sessions can be booked one by one",
            "type": "boolean"
          },
          "allowOverbooking": {
            "type": "boolean"
          },
          "status": {
            "type": "string"
          },
          "sessionCount": {
            "type": "integer"
          },
          "bookedCount": {
            "type": "integer"
          },
          "availableSpots": {
            "type": "integer"
          },
          "sessionsWithCapacity": {
            "type": "integer"
          },
          "requiredSessionsWithCapacity": {
            "type": "integer",
            "nullable": true
          },
          "isBookable": {
            "type": "boolean"
          },
          "isFull": {
            "description": "Full: join the waiting list",
            "type": "boolean"
          },
          "lateSignUp": {
            "description": "Started already (#340): whether and at what price it can still be joined",
            "type": "object"
          },
          "bookingWindow": {
            "$ref": "#/components/schemas/PublicFormBookingWindow"
          },
          "minimumParticipants": {
            "type": "integer",
            "nullable": true
          },
          "trialOffer": {
            "properties": {
              "price": {
                "$ref": "#/components/schemas/PublicFormAmount"
              }
            },
            "type": "object",
            "nullable": true
          },
          "startDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "endDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "firstSessionEnd": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "coachNames": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "sessionDates": {
            "description": "Sessions still to come, not cancelled",
            "type": "array",
            "items": {
              "properties": {
                "startTime": {
                  "type": "string",
                  "format": "date-time"
                },
                "endTime": {
                  "type": "string",
                  "format": "date-time"
                }
              },
              "type": "object"
            }
          }
        },
        "type": "object"
      },
      "PublicSession": {
        "required": [
          "id",
          "startsAt",
          "endsAt",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "startsAt": {
            "type": "string",
            "format": "date-time"
          },
          "endsAt": {
            "type": "string",
            "format": "date-time"
          },
          "price": {
            "$ref": "#/components/schemas/Money"
          },
          "capacity": {
            "type": "integer"
          },
          "spotsAvailable": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "PublicSpotsMap": {
        "description": "A room's spots with their status; never who sits there",
        "properties": {
          "layout": {
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              },
              "columns": {
                "type": "integer"
              },
              "rows": {
                "type": "integer"
              }
            },
            "type": "object"
          },
          "selection": {
            "description": "`customer`: the customer picks a free spot (checkout participants[].spotId)",
            "type": "string",
            "enum": [
              "auto",
              "customer"
            ]
          },
          "spots": {
            "type": "array",
            "items": {
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "x": {
                  "type": "integer"
                },
                "y": {
                  "type": "integer"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "free",
                    "taken",
                    "blocked"
                  ]
                }
              },
              "type": "object"
            }
          },
          "freeCount": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PublicTag": {
        "required": [
          "id",
          "name",
          "slug"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "organisationId": {
            "type": "string",
            "format": "uuid"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "type": "object"
      },
      "PublicWaitlistJoinedEntry": {
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "enum": [
              "session",
              "series"
            ]
          },
          "sessionId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "seriesId": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "status": {
            "type": "string",
            "example": "waiting"
          },
          "place": {
            "description": "Place in line",
            "type": "integer"
          },
          "participantFirstName": {
            "description": "Null for a party",
            "type": "string",
            "nullable": true
          },
          "seats": {
            "description": "Party only: seats it waits for",
            "type": "integer"
          },
          "partySummary": {
            "description": "Party only, e.g. \"2 × Adult, 1 × Child\"",
            "type": "string",
            "nullable": true
          },
          "tickets": {
            "description": "Party only: the tickets it waits for",
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "type": "object"
      },
      "PublicWaitlistParticipantInput": {
        "description": "A saved participant (`participantId`, needs a verified signed-in customer) or a new one by name",
        "properties": {
          "participantId": {
            "type": "string",
            "format": "uuid"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date",
            "nullable": true
          }
        },
        "type": "object"
      },
      "SeriesSettingsCancellationPolicy": {
        "required": [
          "freeCancellationHours",
          "lateCancellation"
        ],
        "properties": {
          "freeCancellationHours": {
            "description": "Customers cancel for free until this many hours before the start",
            "type": "integer"
          },
          "lateCancellation": {
            "description": "What a cancellation after that does",
            "type": "string",
            "enum": [
              "not_allowed",
              "no_refund",
              "partial_refund"
            ]
          },
          "lateRefundPercentage": {
            "description": "Refund share with partial_refund",
            "type": "integer"
          },
          "lateCancellationFee": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SeriesSettingsFee"
              }
            ],
            "nullable": true
          },
          "noShowFee": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/SeriesSettingsFee"
              }
            ],
            "nullable": true,
            "description": "For a series: % of the price per session"
          },
          "entitlementBookings": {
            "description": "Bookings made with a pass/subscription: lose the use (forfeit) or get it back and pay the fee",
            "type": "string",
            "default": "forfeit",
            "enum": [
              "forfeit",
              "fee"
            ]
          }
        },
        "type": "object"
      },
      "SeriesSettingsFee": {
        "description": "A fee: {type: fixed, amount} (cents, in the currency of the booked item) or {type: percentage, percentage} (of the price)",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "fixed",
              "percentage"
            ]
          },
          "amount": {
            "description": "Cents, with type fixed",
            "type": "integer"
          },
          "percentage": {
            "description": "With type percentage",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "StaffErrorWithCode": {
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "description": "Stable error code",
            "type": "string"
          }
        },
        "type": "object"
      },
      "TermsAndConditions": {
        "required": [
          "id",
          "versionNumber",
          "title",
          "content",
          "effectiveFrom"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "versionNumber": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "effectiveFrom": {
            "type": "string",
            "format": "date-time"
          }
        },
        "type": "object"
      },
      "ValidationErrorResponse": {
        "description": "Validation error response with field-level errors",
        "required": [
          "error",
          "errors"
        ],
        "properties": {
          "error": {
            "description": "General error message",
            "type": "string",
            "example": "Validation failed"
          },
          "errors": {
            "description": "Field-level validation errors",
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "type": "object"
      },
      "WidgetConfig": {
        "description": "The widget's settings, in six groups. Unknown keys are refused (422), keys left out take their default, so the format can grow. Ids in `scope` must be the organisation's (422 unknown_reference); `behaviour.handoffUrl` must be on one of its allowed websites.",
        "properties": {
          "scope": {
            "properties": {
              "activityIds": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "locationIds": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "categoryIds": {
                "description": "Tag ids",
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "seriesId": {
                "type": "string",
                "format": "uuid",
                "nullable": true
              },
              "sessionId": {
                "type": "string",
                "format": "uuid",
                "nullable": true
              },
              "mode": {
                "type": "string",
                "default": "both",
                "enum": [
                  "session",
                  "series",
                  "both"
                ]
              },
              "start": {
                "type": "string",
                "default": "activities",
                "enum": [
                  "activities",
                  "locations"
                ]
              }
            },
            "type": "object"
          },
          "show": {
            "properties": {
              "spotsLeft": {
                "type": "string",
                "enum": [
                  "always",
                  "low",
                  "never"
                ]
              },
              "shortDescription": {
                "type": "boolean"
              },
              "price": {
                "type": "boolean"
              },
              "images": {
                "type": "boolean"
              },
              "ageRange": {
                "type": "boolean"
              },
              "location": {
                "type": "boolean"
              },
              "teacher": {
                "type": "boolean"
              },
              "nextDate": {
                "type": "boolean"
              },
              "filters": {
                "type": "boolean"
              }
            },
            "type": "object"
          },
          "layout": {
            "properties": {
              "view": {
                "type": "string",
                "enum": [
                  "grid",
                  "list",
                  "week"
                ]
              },
              "columns": {
                "type": "integer",
                "maximum": 4,
                "minimum": 1
              },
              "limit": {
                "type": "integer",
                "nullable": true,
                "maximum": 100,
                "minimum": 1
              },
              "daysAhead": {
                "type": "integer",
                "maximum": 90,
                "minimum": 1
              },
              "maxDates": {
                "type": "integer",
                "maximum": 50,
                "minimum": 1
              },
              "hideHeader": {
                "type": "boolean"
              },
              "agendaView": {
                "type": "string",
                "default": "list",
                "enum": [
                  "list",
                  "week",
                  "month"
                ]
              },
              "agendaViews": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "list",
                    "week",
                    "month"
                  ]
                }
              }
            },
            "type": "object"
          },
          "look": {
            "properties": {
              "theme": {
                "type": "string",
                "enum": [
                  "branding",
                  "custom"
                ]
              },
              "accent": {
                "description": "A hex colour",
                "type": "string",
                "example": "#1e88e5",
                "nullable": true
              },
              "font": {
                "type": "string",
                "enum": [
                  "inherit",
                  "system",
                  "plek"
                ]
              },
              "radius": {
                "type": "string",
                "enum": [
                  "square",
                  "soft",
                  "round"
                ]
              },
              "density": {
                "type": "string",
                "enum": [
                  "compact",
                  "comfortable"
                ]
              },
              "scheme": {
                "type": "string",
                "enum": [
                  "light",
                  "dark",
                  "auto"
                ]
              },
              "background": {
                "type": "string",
                "enum": [
                  "transparent",
                  "surface"
                ]
              },
              "buttonStyle": {
                "type": "string",
                "enum": [
                  "solid",
                  "outline",
                  "link"
                ]
              }
            },
            "type": "object"
          },
          "texts": {
            "description": "Per text, per language (en, nl, fr, de): own wording or null",
            "properties": {
              "buttonLabel": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "nullable": true
                }
              },
              "emptyText": {
                "type": "object",
                "additionalProperties": {
                  "type": "string",
                  "nullable": true
                }
              }
            },
            "type": "object"
          },
          "behaviour": {
            "properties": {
              "history": {
                "type": "string",
                "enum": [
                  "push",
                  "replace",
                  "none"
                ]
              },
              "handoff": {
                "type": "string",
                "enum": [
                  "inline",
                  "page",
                  "site"
                ]
              },
              "handoffUrl": {
                "type": "string",
                "format": "uri",
                "nullable": true
              },
              "checkout": {
                "type": "string",
                "enum": [
                  "top",
                  "newTab"
                ]
              },
              "language": {
                "description": "page, browser, or fixed:en|nl|fr|de",
                "type": "string",
                "example": "page"
              },
              "scrollOffset": {
                "type": "integer",
                "maximum": 400,
                "minimum": 0
              },
              "minHeight": {
                "type": "integer",
                "maximum": 2000,
                "minimum": 0
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      },
      "WidgetFlow": {
        "description": "What the widget shows",
        "type": "string",
        "enum": [
          "booking",
          "catalogue",
          "activity",
          "agenda",
          "button",
          "gift-cards",
          "request"
        ]
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request - invalid input",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PublicAuthTooManyAttempts": {
        "description": "Too many attempts from this IP address or for this email address; `Retry-After` says when to try again",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "properties": {
                "error": {
                  "type": "string"
                },
                "retryAfter": {
                  "type": "integer"
                }
              },
              "type": "object"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Authentication required",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationErrorResponse"
            }
          }
        }
      }
    },
    "parameters": {
      "OrgSlug": {
        "name": "org_slug",
        "in": "path",
        "description": "Organisation slug identifier",
        "required": true,
        "schema": {
          "type": "string",
          "example": "my-organisation"
        }
      },
      "ResourceId": {
        "name": "id",
        "in": "path",
        "description": "Resource UUID",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    }
  }
}
