{"openapi":"3.1.0","info":{"title":"termine.de Public Booking API","version":"2.0.0","description":"Read-only, unauthenticated API for discovering a business, its services and appointment availability. All\ndates are local \"YYYY-MM-DD\" strings in the Europe/Berlin timezone.\n\nIMPORTANT: there is no programmatic booking-creation endpoint. A booking is always finalized by the user on the\nbooking page: a service's `booking_url` opens that page with the service ALREADY SELECTED (the user then picks\ndate/time and enters their details), while the top-level `booking_page_url` opens the services overview with\nnothing preselected.\n\n## How to use\n1. Discover businesses that offer online bookings with `searchBookableBusinesses` — by location\n   (`category` + `lat`/`lon`, optional `radius`) or by name (`q`). Each result carries a `discovery_url` and a\n   `booking_page_url`. For an exhaustive crawl, the XML sitemap at `/sitemap-index.xml` lists every\n   `/buchen/{slug}/{companyLocationID}` page instead.\n2. `getBookingDiscoveryMetadata` — for a `companyLocationID`, get the business info and its bookable services.\n   Each service includes `available_dates` (the dates within the next 7 days — `availability_window_days` — on\n   which at least one appointment is available) and `next_available_date` (the earliest bookable date overall,\n   which may lie beyond the window; null when nothing is bookable within ~11 months).\n3. To verify a specific other date, call `checkServiceAvailabilityOnDate` (POST) with `companyLocationID`,\n   `serviceID` and `date` — it returns `{ available: true|false }`.\n4. To continue the booking, send the user to the chosen service's `booking_url` — it opens the booking page\n   with that service already selected, so the user only picks a date/time and enters their details. If the user\n   has NOT decided on a service yet (or wants to browse all services), send them to the top-level\n   `booking_page_url` instead — it opens the services overview with nothing preselected. Never put customer\n   data in the link.\n\n## Rules\n- Only services in `services` are bookable online. For a service in `contact_only_services`, do NOT offer a\n  booking link — tell the user to contact the business at the top-level `phone` and relay that service's `info_text`.\n- Modality: use `event_type` / `location_label` (and `room_name` when present) to say whether it is on-site, an\n  online meeting, or a phone appointment — do not assume a physical location.\n- If `online_bookings_enabled` is false, do not offer any booking links — tell the user to contact the business\n  at the top-level `phone` instead.\n- If `available_dates` is empty, there is no availability in the next `availability_window_days` days — offer the\n  user `next_available_date` instead (do NOT probe dates one by one). If `next_available_date` is also null, tell\n  the user nothing is bookable at the moment and point them to `booking_page_url` or the business phone.\n- The availability check returns `available: false` (not an error) for dates in the past or beyond the service's\n  booking horizon. Many businesses only open bookings a few weeks or months ahead, so for a FAR-FUTURE date a\n  `false` often means \"not bookable yet\" rather than \"fully booked\" — say the date cannot be booked (yet) and\n  offer the service's `booking_url` (or the top-level `booking_page_url`) so the user can see the live booking\n  calendar and try for themselves.\n- Availability semantics: `available_dates`, `next_available_date` and the availability check all describe the\n  BASE service with the smallest possible booking (one person, no extra options). `false`/absence is definitive —\n  no selection the user could make would fit. `true`/presence means at least one appointment exists; the exact\n  times on offer depend on the persons and options the user selects on the booking page. Phrase it as \"there are\n  appointments on that date\", not as a guarantee for a specific group size or option combination — and tell the\n  user this is a first check: whether their exact appointment works is confirmed on the booking page, so always\n  include the service's `booking_url` with a positive answer.\n- Errors use the `ErrorResponse` envelope: 404 for unknown `companyLocationID`/`serviceID` — businesses that are\n  not part of the public discovery surface (online booking disabled or not licensed) answer 404 as well, exactly\n  like an unknown id; 400 for invalid input; 5xx are transient — retry later.\n- All endpoints are rate limited per IP. Observe the standard `RateLimit-*` response headers; excess requests\n  receive HTTP 429."},"servers":[{"url":"https://termine.de"}],"paths":{"/api/v1/bookingdiscovery":{"get":{"operationId":"getBookingApiHome","summary":"API index: links to every endpoint and the sitemap","responses":{"200":{"description":"API index","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiHome"}}}}}}},"/api/v1/bookingdiscovery/search":{"get":{"operationId":"searchBookableBusinesses","summary":"Find businesses that offer online bookings, by location or name","description":"Search by location (`category` + `lat`/`lon`, optional `radius` in meters) or by name (`q`, min 2 characters — shorter values return an empty result, not an error). When `lat`/`lon` are present, `q` is ignored. Location results are ordered nearest-first, name results alphabetically. Returns bookable businesses (each with a `discovery_url` for services + availability and a `booking_page_url`). Availability is NOT computed here — GET a result's `discovery_url`. Results are capped (see `max_results`). Only businesses that chose to be publicly listed appear here; the XML sitemap is the exhaustive source of all bookable businesses.","parameters":[{"name":"category","in":"query","required":false,"description":"Business category to filter by (omit or \"all\" for any). Applies to location search.","schema":{"type":"string","enum":["beauty","consulting","education","fitness","hairdresserSalon","photography","healthCare","craft","realEstate","carService","massageTherapy","nailDesign","physiotherapy","saltCave","wellnessTherapy","otherServices","all"]}},{"name":"lat","in":"query","required":false,"description":"Latitude for a location search (pair with lon).","schema":{"type":"number"}},{"name":"lon","in":"query","required":false,"description":"Longitude for a location search (pair with lat).","schema":{"type":"number"}},{"name":"radius","in":"query","required":false,"description":"Search radius in meters (max 500000; DEFAULT 500000 = 500 km when omitted — pass a sensible radius for \"nearby\" questions).","schema":{"type":"integer"}},{"name":"q","in":"query","required":false,"description":"Business name text to search for (alternative to lat/lon; ignored when lat/lon are present).","schema":{"type":"string","minLength":2,"maxLength":100}},{"name":"limit","in":"query","required":false,"description":"Max results to return (capped at max_results).","schema":{"type":"integer"}}],"responses":{"200":{"description":"Matching bookable businesses","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"Invalid input (neither lat/lon nor q provided, or lat/lon out of range)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/bookingdiscovery/{companyLocationID}":{"get":{"operationId":"getBookingDiscoveryMetadata","summary":"Business discovery: location info and bookable services with next-7-days availability","parameters":[{"$ref":"#/components/parameters/companyLocationID"}],"responses":{"200":{"description":"Business metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetMetadata"}}}},"404":{"description":"Unknown companyLocationID, or a business that is not part of the public discovery surface (online booking disabled or not licensed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/bookingdiscovery/availability":{"post":{"operationId":"checkServiceAvailabilityOnDate","summary":"Check whether a service has any appointment available on a specific date","description":"Returns whether at least one appointment slot exists for the service on the given date (any employee, smallest possible booking: one person, no extra options). `false` is definitive; on `true` the exact times on offer depend on the persons/options the user selects on the booking page. Use to verify a specific date beyond the availability_window_days window exposed in the discovery metadata. Dates in the past or beyond the service's booking horizon return `available: false` (not an error).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityCheckRequest"}}}},"responses":{"200":{"description":"Availability result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityCheckResponse"}}}},"400":{"description":"Invalid input (missing field, malformed date)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Unknown companyLocationID or serviceID — including services that are not publicly bookable and businesses that are not part of the public discovery surface (online booking disabled or not licensed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/bookingdiscovery/openapi.json":{"get":{"operationId":"getBookingApiSpec","summary":"This OpenAPI 3.1 document","responses":{"200":{"description":"OpenAPI document","content":{"application/json":{}}}}}}},"components":{"parameters":{"companyLocationID":{"name":"companyLocationID","in":"path","required":true,"schema":{"type":"string"}}},"schemas":{"ErrorResponse":{"type":"object","required":["error"],"description":"Error envelope returned with HTTP 400 (invalid input) and 404 (unknown companyLocationID or serviceID; businesses with online booking disabled or without an active license answer 404 too). 5xx responses (INTERNAL_SERVER_ERROR) are transient — retry later.","properties":{"error":{"type":"object","required":["code"],"properties":{"code":{"type":"string","description":"Machine-readable error code.","examples":["INPUT_INVALID","ITEM_NOT_FOUND"]},"properties":{"type":"object","description":"Optional additional detail (e.g. which input field failed validation)."}}}}},"ApiHome":{"type":"object","properties":{"name":{"type":"string"},"timezone":{"type":"string","examples":["Europe/Berlin"]},"openapi_url":{"type":"string","format":"uri"},"search_url":{"type":"string","format":"uri"},"metadata_url_template":{"type":"string","description":"Substitute {companyLocationID}."},"availability_check_url":{"type":"string","format":"uri"},"sitemap_url":{"type":"string","format":"uri"},"search_categories":{"type":"array","items":{"type":"string"},"description":"Valid values for the search endpoint's category parameter."},"usage":{"type":"string"}}},"SearchResponse":{"type":"object","required":["count","max_results","results"],"properties":{"count":{"type":"integer","description":"Number of results returned."},"max_results":{"type":"integer","description":"The server-side cap on results per search."},"results":{"type":"array","items":{"$ref":"#/components/schemas/Location"}}}},"Location":{"type":"object","required":["id","name","booking_page_url","discovery_url"],"properties":{"id":{"type":"string","description":"companyLocationID — pass to the metadata / availability endpoints."},"name":{"type":"string"},"address":{"type":"string"},"geo":{"type":"object","description":"Absent when the business has no coordinates.","properties":{"lat":{"type":"number"},"lon":{"type":"number"}}},"category":{"type":"string"},"distance_m":{"type":"integer","description":"Distance from the searched point in meters (only present for location search)."},"booking_page_url":{"type":"string","format":"uri","description":"Booking page of the business (services overview, no service preselected)."},"discovery_url":{"type":"string","format":"uri","description":"GET this for the business's services + availability summary."}}},"WidgetMetadata":{"type":"object","required":["id","name","timezone","online_bookings_enabled","booking_page_url","availability_window_days","availability_check_url","services"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"address":{"type":"string"},"geo":{"type":"object","description":"Absent when the business has no coordinates.","properties":{"lat":{"type":"number"},"lon":{"type":"number"}}},"phone":{"type":"string"},"website":{"type":"string"},"timezone":{"type":"string","examples":["Europe/Berlin"]},"online_bookings_enabled":{"type":"boolean","description":"False when the business currently has no online-bookable service — services will be empty; do not offer booking links, point the user to phone instead. (Businesses with online booking disabled or without an active license do not appear here at all — they answer 404.)"},"booking_page_url":{"type":"string","format":"uri","description":"Booking page showing the services overview — no service preselected. Send the user here when they have not chosen a service yet or want to browse; for a specific service use that service's booking_url instead."},"availability_window_days":{"type":"integer","description":"Number of days ahead (from today, inclusive) covered by each service's available_dates."},"availability_check_url":{"type":"string","format":"uri","description":"POST { companyLocationID, serviceID, date } here to check availability on a specific date (e.g. beyond availability_window_days)."},"openapi_url":{"type":"string","format":"uri"},"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"contact_only_services":{"type":"array","description":"Services the business offers but that cannot be booked online. Tell the user to contact the location (see top-level phone); info_text is the provider's note. ABSENT (not empty) when no such services exist.","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"info_text":{"type":"string"}}}}}},"Service":{"type":"object","required":["id","name","duration_minutes","booking_url","available_dates","next_available_date"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"price_eur":{"type":"number"},"event_type":{"type":"string","enum":["EVENT_AT_COMPANY_LOCATION","EVENT_ONLINE_GENERIC","EVENT_PHONE"],"description":"Appointment modality: on-site / online meeting / phone."},"location_label":{"type":"string","description":"Human-readable modality label (e.g. \"Telefontermin\", \"Online-Meeting\", \"Vor Ort am Firmenstandort\")."},"room_name":{"type":"string","description":"Room name for on-site appointments (only present for EVENT_AT_COMPANY_LOCATION with a room set)."},"booking_url":{"type":"string","format":"uri","description":"Opens the booking page with THIS service already selected (ends with ?serviceID=...); the user picks a date/time there and completes the booking. For a non-preselected entry (services overview) use the top-level booking_page_url instead."},"available_dates":{"type":"array","items":{"type":"string","format":"date"},"description":"Dates (\"YYYY-MM-DD\", Europe/Berlin) within the next availability_window_days days on which at least one appointment is available for this service (across any employee, for the smallest possible booking — see the Rules section on availability semantics). Empty if none."},"next_available_date":{"type":["string","null"],"format":"date","description":"Earliest bookable date overall — equals available_dates[0] when that list is non-empty, otherwise the first free date beyond the window (scanned ~11 months ahead). null when nothing is bookable in that horizon. Offer this instead of probing dates one by one."}}},"AvailabilityCheckRequest":{"type":"object","required":["companyLocationID","serviceID","date"],"properties":{"companyLocationID":{"type":"string"},"serviceID":{"type":"string"},"date":{"type":"string","format":"date","description":"The date to check, \"YYYY-MM-DD\" (Europe/Berlin)."}}},"AvailabilityCheckResponse":{"type":"object","required":["available","companyLocationID","serviceID","date"],"properties":{"available":{"type":"boolean","description":"True if at least one appointment slot is available for the service on the date (smallest possible booking: one person, no extra options — final times depend on the selections the user makes on the booking page; false is definitive). Also false (not an error) for past dates and dates beyond the service's booking horizon."},"companyLocationID":{"type":"string"},"serviceID":{"type":"string"},"date":{"type":"string","format":"date"}}}}}}