{
  "openapi": "3.1.0",
  "info": {
    "title": "The Great Escape — publieke discovery-API",
    "version": "1.0.0",
    "description": "Read-only API met de experience-catalogus en live beschikbaarheid van The Great Escape in Zwolle. Publiek: geen API-key of login nodig. Alleen GET. Er kan via deze API niet geboekt worden; boeken gebeurt via de bookingUrl op thegreatescape.nl. Beoordelingen in externalRatings komen van externe platforms en zijn geen eigen aggregateRating.",
    "contact": {
      "url": "https://thegreatescape.nl/contact"
    }
  },
  "servers": [
    {
      "url": "https://api.thegreatescape.nl",
      "description": "Publieke API"
    }
  ],
  "paths": {
    "/availability": {
      "get": {
        "summary": "Live beschikbare starttijden",
        "description": "Beschikbare starttijden voor een datum en groepsgrootte. Zonder 'room' worden alle actieve belevingen teruggegeven waarvoor de groepsgrootte binnen min/max valt; belevingen die daarbuiten vallen worden stil overgeslagen. Antwoorden worden circa 60 seconden gecachet.",
        "operationId": "getAvailability",
        "parameters": [
          {
            "name": "room",
            "in": "query",
            "required": false,
            "description": "Slug van de beleving. Weglaten om alle passende belevingen te krijgen.",
            "schema": {
              "type": "string"
            },
            "example": "the-courtroom"
          },
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Datum in formaat YYYY-MM-DD, vandaag tot maximaal 12 maanden vooruit.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-05"
          },
          {
            "name": "players",
            "in": "query",
            "required": true,
            "description": "Aantal spelers (geheel getal).",
            "schema": {
              "type": "integer",
              "minimum": 1
            },
            "example": 5
          }
        ],
        "responses": {
          "200": {
            "description": "Beschikbaarheid.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SingleAvailability"
                    },
                    {
                      "$ref": "#/components/schemas/MultiAvailability"
                    }
                  ]
                },
                "examples": {
                  "single": {
                    "summary": "GET /availability?room=the-courtroom&date=2026-09-05&players=5",
                    "value": {
                      "location": "The Great Escape Zwolle",
                      "room": {
                        "slug": "the-courtroom",
                        "name": "The Courtroom",
                        "minPlayers": 3,
                        "maxPlayers": 6,
                        "durationMinutes": 75
                      },
                      "date": "2026-09-05",
                      "players": 5,
                      "timezone": "Europe/Amsterdam",
                      "updatedAt": "2026-09-01T11:20:00+02:00",
                      "slots": [
                        {
                          "start": "19:30",
                          "available": true,
                          "price": 180,
                          "currency": "EUR",
                          "bookingUrl": "https://thegreatescape.nl/reserveren?room=the-courtroom&date=2026-09-05&time=19%3A30&players=5"
                        }
                      ]
                    }
                  },
                  "allRooms": {
                    "summary": "GET /availability?date=2026-09-05&players=5",
                    "value": {
                      "location": "The Great Escape Zwolle",
                      "date": "2026-09-05",
                      "players": 5,
                      "timezone": "Europe/Amsterdam",
                      "updatedAt": "2026-09-01T11:20:00+02:00",
                      "experiences": [
                        {
                          "room": {
                            "slug": "the-mission",
                            "name": "The Mission",
                            "minPlayers": 3,
                            "maxPlayers": 7,
                            "durationMinutes": 75
                          },
                          "slots": [
                            {
                              "start": "14:00",
                              "available": true,
                              "price": null,
                              "currency": "EUR",
                              "bookingUrl": "https://thegreatescape.nl/reserveren?room=the-mission&date=2026-09-05&time=14%3A00&players=5"
                            }
                          ]
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Ongeldige of ontbrekende parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_date": {
                    "value": {
                      "error": "missing_date",
                      "message": "Parameter 'date' is verplicht (YYYY-MM-DD)."
                    }
                  },
                  "players_above_maximum": {
                    "value": {
                      "error": "players_above_maximum",
                      "message": "Deze kamer heeft maximaal 6 spelers."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Onbekende kamer of beleving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unknown_room": {
                    "value": {
                      "error": "unknown_room",
                      "message": "Onbekende of inactieve escape room: 'foo'."
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Beschikbaarheid tijdelijk niet op te vragen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/availability/{slug}": {
      "get": {
        "summary": "Datumindex van een beleving (hypermedia)",
        "description": "Compacte lijst van de komende 60 datums met per datum een directe URL naar de starttijden van die dag. Deze index bevat geen beschikbaarheid: uitsluitend de datum-resource stelt beschikbaarheid vast. Antwoorden worden circa 5 minuten gecachet.",
        "operationId": "getAvailabilityDates",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "the-courtroom"
          }
        ],
        "responses": {
          "200": {
            "description": "Datumindex.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityDates"
                }
              }
            }
          },
          "404": {
            "description": "Onbekende of inactieve beleving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unknown_experience": {
                    "value": {
                      "error": "unknown_experience",
                      "message": "Onbekende of inactieve beleving: 'foo'."
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/availability/{slug}/{date}": {
      "get": {
        "summary": "Beschikbare starttijden op één datum (hypermedia)",
        "description": "Dezelfde bron en dezelfde prijslogica als /availability. Beschikbaarheid hangt niet af van het aantal spelers: elk genoemd tijdslot is geschikt voor elke groepsgrootte binnen de capaciteit van de beleving. Antwoorden worden circa 60 seconden gecachet.",
        "operationId": "getAvailabilityDay",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "the-courtroom"
          },
          {
            "name": "date",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-09-12"
          }
        ],
        "responses": {
          "200": {
            "description": "Beschikbare starttijden.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AvailabilityDay"
                }
              }
            }
          },
          "400": {
            "description": "Ongeldige of ontbrekende parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "missing_date": {
                    "value": {
                      "error": "missing_date",
                      "message": "Parameter 'date' is verplicht (YYYY-MM-DD)."
                    }
                  },
                  "players_above_maximum": {
                    "value": {
                      "error": "players_above_maximum",
                      "message": "Deze kamer heeft maximaal 6 spelers."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Onbekende of inactieve beleving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Beschikbaarheid tijdelijk niet op te vragen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/docs": {
      "get": {
        "summary": "HTML discovery-pagina met klikbare links",
        "description": "Menselijk én machineleesbare HTML-pagina met echte <a href>-links naar de catalogus, de OpenAPI-beschrijving en de datumindex van elke actieve beleving. Geen JavaScript nodig.",
        "operationId": "getDocs",
        "responses": {
          "200": {
            "description": "HTML-pagina.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/experiences": {
      "get": {
        "summary": "Catalogus van alle actieve belevingen",
        "operationId": "listExperiences",
        "responses": {
          "200": {
            "description": "Alle actieve belevingen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExperienceList"
                },
                "examples": {
                  "list": {
                    "summary": "GET /experiences",
                    "value": {
                      "business": {
                        "name": "The Great Escape",
                        "city": "Zwolle",
                        "country": "NL",
                        "timezone": "Europe/Amsterdam",
                        "website": "https://thegreatescape.nl"
                      },
                      "updatedAt": "2026-09-01T11:20:00+02:00",
                      "openapi": "https://api.thegreatescape.nl/openapi.json",
                      "experiences": [
                        {
                          "slug": "the-courtroom",
                          "name": "The Courtroom",
                          "type": "escape-room",
                          "indoor": true,
                          "capacity": {
                            "min": 3,
                            "max": 6
                          },
                          "durationMinutes": 75,
                          "difficulty": 4,
                          "minAgeYears": 14,
                          "minAgeNote": "Minimaal 14 jaar",
                          "isHorror": false,
                          "suitableFor": [],
                          "languages": [
                            "nl",
                            "en"
                          ],
                          "externalRatings": [
                            {
                              "source": "escapetalk.nl",
                              "score": 9,
                              "scoreMax": 10,
                              "reviewCount": 236,
                              "url": "https://escapetalk.nl/en/escaperoom/the-great-escape/"
                            }
                          ],
                          "variants": [
                            {
                              "mode": "battle",
                              "capacity": {
                                "min": 7,
                                "max": 12
                              },
                              "durationMinutes": 75
                            }
                          ],
                          "officialUrl": "https://thegreatescape.nl/escaperooms/the-courtroom",
                          "bookingUrl": "https://thegreatescape.nl/reserveren?room=the-courtroom",
                          "availabilityUrlTemplate": "https://api.thegreatescape.nl/availability?room=the-courtroom&date={YYYY-MM-DD}&players={n}"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/experiences/{slug}": {
      "get": {
        "summary": "Eén beleving",
        "operationId": "getExperience",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "the-courtroom"
          }
        ],
        "responses": {
          "200": {
            "description": "De beleving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExperienceDetail"
                }
              }
            }
          },
          "404": {
            "description": "Onbekende of inactieve beleving.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "unknown_experience": {
                    "value": {
                      "error": "unknown_experience",
                      "message": "Onbekende of inactieve beleving: 'foo'."
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "Andere methode dan GET.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "method_not_allowed": {
                    "value": {
                      "error": "method_not_allowed",
                      "message": "Alleen GET wordt ondersteund."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "Deze machineleesbare beschrijving",
        "operationId": "getOpenApi",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1-document."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "missing_date",
              "invalid_date",
              "date_in_past",
              "date_too_far",
              "missing_players",
              "invalid_players",
              "players_below_minimum",
              "players_above_maximum",
              "unknown_room",
              "unknown_experience",
              "method_not_allowed"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Slot": {
        "type": "object",
        "required": [
          "start",
          "available",
          "price",
          "currency",
          "bookingUrl"
        ],
        "properties": {
          "start": {
            "type": "string",
            "examples": [
              "19:30"
            ]
          },
          "available": {
            "type": "boolean",
            "const": true
          },
          "price": {
            "type": [
              "number",
              "null"
            ],
            "description": "Totaalprijs voor de groep in euro, of null wanneer die niet betrouwbaar bekend is."
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "bookingUrl": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Room": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "minPlayers": {
            "type": [
              "integer",
              "null"
            ]
          },
          "maxPlayers": {
            "type": [
              "integer",
              "null"
            ]
          },
          "durationMinutes": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Business": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "examples": [
              "The Great Escape"
            ]
          },
          "city": {
            "type": "string",
            "examples": [
              "Zwolle"
            ]
          },
          "country": {
            "type": "string",
            "examples": [
              "NL"
            ]
          },
          "timezone": {
            "type": "string",
            "examples": [
              "Europe/Amsterdam"
            ]
          },
          "website": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Experience": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "type",
          "capacity",
          "officialUrl",
          "bookingUrl",
          "availabilityUrlTemplate"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "shortDescription": {
            "type": [
              "string",
              "null"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "escape-room",
              "outdoor-game",
              "mini-escape"
            ]
          },
          "indoor": {
            "type": "boolean"
          },
          "capacity": {
            "type": "object",
            "properties": {
              "min": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "max": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "durationMinutes": {
            "type": [
              "integer",
              "null"
            ]
          },
          "difficulty": {
            "type": [
              "number",
              "null"
            ],
            "description": "Schaal 1-5."
          },
          "minAgeYears": {
            "type": [
              "integer",
              "null"
            ]
          },
          "minAgeNote": {
            "type": [
              "string",
              "null"
            ]
          },
          "isHorror": {
            "type": "boolean"
          },
          "hasActors": {
            "type": "boolean"
          },
          "wheelchairAccessible": {
            "type": "boolean"
          },
          "epilepsyWarning": {
            "type": "boolean"
          },
          "suitableFor": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "bedrijfsuitje",
                "teambuilding",
                "kinderfeestje",
                "vrijgezellenfeest",
                "date-night",
                "familie-uitje"
              ]
            }
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "priceFrom": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Vanaf-prijs in euro."
          },
          "priceNote": {
            "type": "string"
          },
          "languages": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "externalRatings": {
            "type": "array",
            "description": "Beoordelingen van externe platforms, expliciet toegeschreven aan hun bron. Geen eigen aggregateRating.",
            "items": {
              "type": "object",
              "properties": {
                "source": {
                  "type": "string",
                  "examples": [
                    "escapetalk.nl"
                  ]
                },
                "score": {
                  "type": "number"
                },
                "scoreMax": {
                  "type": "number",
                  "examples": [
                    10
                  ]
                },
                "reviewCount": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "url": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uri"
                }
              }
            }
          },
          "variants": {
            "type": "array",
            "description": "Alternatieve speelmodi van dezelfde ruimte, bijvoorbeeld battle.",
            "items": {
              "type": "object",
              "properties": {
                "mode": {
                  "type": "string",
                  "enum": [
                    "battle"
                  ]
                },
                "capacity": {
                  "type": "object",
                  "properties": {
                    "min": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "max": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                },
                "durationMinutes": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          },
          "officialUrl": {
            "type": "string",
            "format": "uri"
          },
          "bookingUrl": {
            "type": "string",
            "format": "uri"
          },
          "availabilityUrlTemplate": {
            "type": "string",
            "description": "Vul {YYYY-MM-DD} en {n} in. Zonder date en players geeft /availability een 400.",
            "examples": [
              "https://api.thegreatescape.nl/availability?room=the-courtroom&date={YYYY-MM-DD}&players={n}"
            ]
          }
        }
      },
      "SingleAvailability": {
        "type": "object",
        "description": "Respons wanneer 'room' is meegegeven.",
        "properties": {
          "location": {
            "type": "string"
          },
          "room": {
            "$ref": "#/components/schemas/Room"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "players": {
            "type": "integer"
          },
          "timezone": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          }
        }
      },
      "MultiAvailability": {
        "type": "object",
        "description": "Respons wanneer 'room' is weggelaten.",
        "properties": {
          "location": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "players": {
            "type": "integer"
          },
          "timezone": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "experiences": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "room": {
                  "$ref": "#/components/schemas/Room"
                },
                "slots": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Slot"
                  }
                }
              }
            }
          }
        }
      },
      "HypermediaExperience": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "capacity": {
            "type": "object",
            "properties": {
              "min": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "max": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "AvailabilityDates": {
        "type": "object",
        "description": "Datumindex van één beleving. Bevat geen beschikbaarheid.",
        "properties": {
          "experience": {
            "$ref": "#/components/schemas/HypermediaExperience"
          },
          "timezone": {
            "type": "string"
          },
          "generated_at": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "dates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "links": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "AvailabilityDay": {
        "type": "object",
        "description": "Beschikbare starttijden van één beleving op één datum.",
        "properties": {
          "experience": {
            "$ref": "#/components/schemas/HypermediaExperience"
          },
          "date": {
            "type": "string",
            "format": "date"
          },
          "timezone": {
            "type": "string"
          },
          "generated_at": {
            "type": "string"
          },
          "note": {
            "type": "string"
          },
          "slots": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "start",
                "price",
                "currency",
                "booking_url"
              ],
              "properties": {
                "start": {
                  "type": "string",
                  "examples": [
                    "14:00"
                  ]
                },
                "price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Totaalprijs voor de groep in euro, of null wanneer die niet betrouwbaar bekend is."
                },
                "currency": {
                  "type": "string",
                  "const": "EUR"
                },
                "booking_url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "links": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "format": "uri"
            }
          }
        }
      },
      "ExperienceList": {
        "type": "object",
        "properties": {
          "business": {
            "$ref": "#/components/schemas/Business"
          },
          "updatedAt": {
            "type": "string"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "experiences": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Experience"
            }
          }
        }
      },
      "ExperienceDetail": {
        "type": "object",
        "properties": {
          "business": {
            "$ref": "#/components/schemas/Business"
          },
          "updatedAt": {
            "type": "string"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "experience": {
            "$ref": "#/components/schemas/Experience"
          }
        }
      }
    }
  }
}