{
  "openapi": "3.0.3",
  "info": {
    "title": "VenueFlow Booking API",
    "version": "1.0.0",
    "description": "Publieke boekings-API van VenueFlow. Hiermee bouw je een eigen boek-frontend voor een venue: faciliteiten en beschikbare tijden ophalen en een boeking aanmaken. Dit is dezelfde API die de officiële VenueFlow-widget gebruikt.\n\n**Authenticatie** — de publieke endpoints hebben geen API-sleutel; je geeft de venue op via de `X-Tenant`-header (de slug van de venue). Whitelist je website-origin in de admin van de venue (per-venue toegestane origins / eigen domein), anders blokkeert de browser de aanroep via CORS.",
    "contact": { "name": "VenueFlow", "url": "https://venueflow.eu" }
  },
  "servers": [
    { "url": "https://api.venueflow.eu", "description": "VenueFlow API (of je eigen API-domein, bijv. https://boeken.jouwvenue.nl)" }
  ],
  "tags": [
    { "name": "Booking", "description": "De publieke boek-flow: faciliteiten → tijden → boeken." }
  ],
  "paths": {
    "/api/site/facilities": {
      "get": {
        "tags": ["Booking"],
        "summary": "Faciliteiten ophalen",
        "description": "Alle boekbare faciliteiten van de venue, plus de configuratie voor de boek-flow (dag-grens en extra velden).",
        "parameters": [ { "$ref": "#/components/parameters/Tenant" } ],
        "responses": {
          "200": {
            "description": "Lijst met faciliteiten.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FacilitiesResponse" },
              "example": {
                "live": true,
                "day_start": "00:00",
                "facilities": [
                  { "id": 1, "name": "Privésauna", "mode": "FULL", "capacity": 1, "min_people": 1, "max_people_per_booking": 1, "duration_minutes": 90, "price_eur": 45, "open_days": [1,2,3,4,5,6,7], "active": true, "images": [], "description": "" },
                  { "id": 2, "name": "Wellness dagpas", "mode": "CAPACITY", "capacity": 20, "min_people": 1, "max_people_per_booking": 8, "duration_minutes": 60, "price_eur": 27.5, "open_days": [1,2,3,4,5,6,7], "active": true, "images": [], "description": "" }
                ],
                "fields": [ { "key": "telefoon", "label": "Telefoon", "type": "tel", "required": true } ]
              }
            } }
          },
          "400": { "$ref": "#/components/responses/NoTenant" }
        }
      }
    },
    "/api/site/facilities/{id}/slots": {
      "get": {
        "tags": ["Booking"],
        "summary": "Beschikbare tijden ophalen",
        "description": "De boekbare tijdslots van één faciliteit op een gekozen dag.",
        "parameters": [
          { "$ref": "#/components/parameters/Tenant" },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Faciliteit-id." },
          { "name": "date", "in": "query", "required": false, "schema": { "type": "string", "format": "date", "example": "2026-09-01" }, "description": "Dag (YYYY-MM-DD). Standaard vandaag." }
        ],
        "responses": {
          "200": {
            "description": "Tijdslots voor de dag.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SlotsResponse" },
              "example": {
                "facility": "Wellness dagpas", "date": "2026-09-01", "day_start": "00:00",
                "slots": [
                  { "id": 2101, "starts_at": "2026-09-01T09:00", "ends_at": "2026-09-01T10:00", "capacity": 20, "booked": 3, "free": 17, "blocked": false, "bookable": true },
                  { "id": 2102, "starts_at": "2026-09-01T10:00", "ends_at": "2026-09-01T11:00", "capacity": 20, "booked": 20, "free": 0, "blocked": false, "bookable": false }
                ]
              }
            } }
          },
          "400": { "$ref": "#/components/responses/NoTenant" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/site/facilities/{id}/availability": {
      "get": {
        "tags": ["Booking"],
        "summary": "Beschikbaarheid per dag (bereik)",
        "description": "Per dag over een bereik: is er iets te boeken en hoeveel vrij. Ideaal voor een kalender/maandweergave — zonder per dag een aparte call. Bereik max 62 dagen.",
        "parameters": [
          { "$ref": "#/components/parameters/Tenant" },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Faciliteit-id." },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Startdag (YYYY-MM-DD). Standaard vandaag." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Einddag. Standaard from + 30 dagen (max 62)." }
        ],
        "responses": {
          "200": {
            "description": "Beschikbaarheid per dag.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RangeAvailability" },
              "example": { "facility": "Wellness dagpas", "from": "2026-09-01", "to": "2026-09-03",
                "days": [ { "date": "2026-09-01", "free": 42, "bookable": true }, { "date": "2026-09-02", "free": 0, "bookable": false }, { "date": "2026-09-03", "free": 18, "bookable": true } ] }
            } }
          },
          "400": { "$ref": "#/components/responses/NoTenant" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/site/slots/{id}": {
      "get": {
        "tags": ["Booking"],
        "summary": "Eén slot herverifiëren",
        "description": "Controleer vlak vóór het boeken of een specifiek slot nog beschikbaar is. Lichter dan de dag-lijst — handig voor een 'nog vrij?'-check of vlak voor submit.",
        "parameters": [
          { "$ref": "#/components/parameters/Tenant" },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "Slot-id." }
        ],
        "responses": {
          "200": {
            "description": "Actuele beschikbaarheid van het slot.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SlotCheck" },
              "example": { "id": 2101, "facility_id": 2, "starts_at": "2026-09-01T09:00", "ends_at": "2026-09-01T10:00", "capacity": 20, "free": 17, "bookable": true }
            } }
          },
          "400": { "$ref": "#/components/responses/NoTenant" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/site/bookings": {
      "post": {
        "tags": ["Booking"],
        "summary": "Boeking aanmaken",
        "description": "Maakt een boeking op een tijdslot. Heeft de faciliteit een prijs én is Mollie gekoppeld, dan bevat het antwoord een `payment.checkout_url` waar je de gast naartoe stuurt; na betalen komt hij terug op je `redirect_url`. Anders is de boeking meteen bevestigd.",
        "parameters": [ { "$ref": "#/components/parameters/Tenant" } ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingRequest" },
            "example": { "slot_id": 2101, "customer_name": "Petra Jansen", "customer_email": "petra@voorbeeld.nl", "people": 2, "extra": { "telefoon": "0612345678" }, "locale": "nl", "redirect_url": "https://www.jouwvenue.nl/bedankt" }
          } }
        },
        "responses": {
          "201": {
            "description": "Boeking aangemaakt.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingResponse" },
              "example": {
                "booking": { "id": 812, "facility": "Wellness dagpas", "starts_at": "2026-09-01T09:00", "ends_at": "2026-09-01T10:00", "people": 2, "amount_eur": 55, "status": "pending_payment", "customer_name": "Petra Jansen", "customer_email": "petra@voorbeeld.nl" },
                "ticket_url": "https://app.venueflow.eu/ticket?t=…",
                "payment": { "status": "open", "checkout_url": "https://pay.mollie.com/...", "platform_fee_eur": 0.5 }
              }
            } }
          },
          "400": { "$ref": "#/components/responses/NoTenant" },
          "422": { "$ref": "#/components/responses/Unprocessable" },
          "429": { "description": "Te veel boekingen vanaf dit IP (max 20/uur).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Tenant": { "name": "X-Tenant", "in": "header", "required": true, "schema": { "type": "string", "example": "sauna-de-bron" }, "description": "De slug van de venue." }
    },
    "responses": {
      "NoTenant": { "description": "Geen venue opgegeven (X-Tenant-header of subdomein ontbreekt).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "Niet gevonden.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unprocessable": { "description": "Validatiefout of niet-boekbaar (bijv. venue niet live, slot vol).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "FacilitiesResponse": {
        "type": "object",
        "properties": {
          "live": { "type": "boolean", "description": "Neemt de venue online boekingen aan? Zo niet, toon een 'binnenkort'-melding." },
          "day_start": { "type": "string", "example": "00:00", "description": "Start van de 'boek-dag' (HH:MM)." },
          "facilities": { "type": "array", "items": { "$ref": "#/components/schemas/Facility" } },
          "fields": { "type": "array", "items": { "$ref": "#/components/schemas/BookingField" }, "description": "Extra velden die de venue bij een boeking wil verzamelen." }
        }
      },
      "Facility": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "mode": { "type": "string", "enum": ["CAPACITY", "FULL"], "description": "CAPACITY = meerdere gasten per slot; FULL = exclusief (privé)." },
          "capacity": { "type": "integer" },
          "min_people": { "type": "integer" },
          "max_people_per_booking": { "type": "integer" },
          "duration_minutes": { "type": "integer" },
          "price_eur": { "type": "number", "description": "Prijs per persoon (0 = gratis)." },
          "open_days": { "type": "array", "items": { "type": "integer" }, "description": "Open weekdagen: 1=maandag … 7=zondag." },
          "active": { "type": "boolean" },
          "images": { "type": "array", "items": { "type": "string" } },
          "description": { "type": "string" }
        }
      },
      "BookingField": {
        "type": "object",
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "type": { "type": "string", "enum": ["text", "tel", "select", "checkbox", "textarea"] },
          "required": { "type": "boolean" },
          "options": { "type": "array", "items": { "type": "string" }, "description": "Alleen bij type=select." }
        }
      },
      "SlotsResponse": {
        "type": "object",
        "properties": {
          "facility": { "type": "string" },
          "date": { "type": "string", "format": "date" },
          "day_start": { "type": "string" },
          "slots": { "type": "array", "items": { "$ref": "#/components/schemas/Slot" } }
        }
      },
      "Slot": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "starts_at": { "type": "string", "example": "2026-09-01T09:00", "description": "Lokale tijd van de venue (geen tijdzone-suffix)." },
          "ends_at": { "type": "string", "example": "2026-09-01T10:00" },
          "capacity": { "type": "integer" },
          "booked": { "type": "integer" },
          "free": { "type": "integer", "description": "Nog beschikbare plekken." },
          "blocked": { "type": "boolean" },
          "bookable": { "type": "boolean", "description": "Handig: true = te boeken (niet vol, niet geblokkeerd)." }
        }
      },
      "RangeAvailability": {
        "type": "object",
        "properties": {
          "facility": { "type": "string" },
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "days": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": { "type": "string", "format": "date" },
                "free": { "type": "integer", "description": "Totaal vrije plekken die dag." },
                "bookable": { "type": "boolean", "description": "Is er iets te boeken die dag?" }
              }
            }
          }
        }
      },
      "SlotCheck": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "facility_id": { "type": "integer" },
          "starts_at": { "type": "string" },
          "ends_at": { "type": "string" },
          "capacity": { "type": "integer" },
          "free": { "type": "integer" },
          "bookable": { "type": "boolean" }
        }
      },
      "BookingRequest": {
        "type": "object",
        "required": ["slot_id", "customer_name", "customer_email"],
        "properties": {
          "slot_id": { "type": "integer" },
          "customer_name": { "type": "string" },
          "customer_email": { "type": "string", "format": "email" },
          "people": { "type": "integer", "default": 1, "description": "Aantal personen (bij CAPACITY-faciliteiten)." },
          "extra": { "type": "object", "additionalProperties": true, "description": "Waarden voor de extra velden uit /facilities (key → waarde)." },
          "locale": { "type": "string", "enum": ["nl", "en"], "default": "nl" },
          "redirect_url": { "type": "string", "description": "Waar de gast na betalen terugkomt." }
        }
      },
      "BookingResponse": {
        "type": "object",
        "properties": {
          "booking": { "type": "object", "description": "De aangemaakte boeking." },
          "ticket_url": { "type": "string", "description": "Getekende link naar de ticketpagina (bekijken/annuleren). Toon 'm aan de gast of stuur 'm door." },
          "payment": {
            "type": "object",
            "description": "Alleen aanwezig als er betaald moet worden.",
            "properties": {
              "status": { "type": "string" },
              "checkout_url": { "type": "string", "description": "Stuur de gast hiernaartoe om te betalen." },
              "platform_fee_eur": { "type": "number" }
            }
          }
        }
      },
      "Error": { "type": "object", "properties": { "error": { "type": "string" } } }
    }
  }
}
