{"openapi":"3.1.0","info":{"title":"Durable Booking API","version":"1.1.0","description":"Public read-only API for checking booking availability, listing services, and finding available time slots on a Durable website.\n\nTypical agent workflow: 1) GET /api/booking/availability to confirm bookings are open; 2) GET /api/booking/services to list services and read each service's `preBookingQuestions`; 3) GET /api/booking/time-slots?serviceId=...&date=YYYY-MM-DD to get available `slots`; 4) return the provided `bookingUrl` to the customer, optionally appending supported customer prefill query params. The customer must click Confirm booking in the widget. This API does not create bookings directly.\n\nAll responses are JSON. Time fields are UTC ISO 8601 strings.","x-booking-deep-link":{"description":"Every GET response includes pre-built booking deep links so agents can return a URL to a user without constructing it manually. The link opens the booking widget and pre-fills the specified state. The user lands on the deepest valid step and clicks 'Confirm booking' to submit - the link never auto-books.","urlTemplate":"https://{domain}/?booking=open&service={serviceId}&date={YYYY-MM-DD}&slot={UTC-ISO}&firstName=...&email=...","params":{"booking":{"type":"string","enum":["open"],"description":"Opens the widget."},"service":{"type":"integer","description":"Pre-selects the service; advances to step 2."},"date":{"type":"string","format":"date","description":"Pre-selects the date in step 2 (YYYY-MM-DD, business timezone)."},"slot":{"type":"string","format":"date-time","description":"Pre-selects a time slot; advances to step 3 after the slot loads."},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string","description":"Maps to phoneNumber in the customer form."},"address":{"type":"string"},"answers":{"type":"string","description":"URL-encoded JSON array of {question, answer} matching the service's preBookingQuestions."}},"stepSemantics":"no params -> widget open at step 1; service only -> step 2; service+date -> step 2 with date selected; service+date+slot -> step 3 after slot loads; all fields -> step 3 with Confirm enabled. Never auto-submits."}},"servers":[{"url":"/","description":"Current Durable website host."}],"tags":[{"name":"Booking","description":"Read-only booking discovery endpoints. Return a bookingUrl to the customer; bookings are confirmed in the website widget."}],"paths":{"/api/booking/availability":{"get":{"tags":["Booking"],"operationId":"getBookingAvailability","summary":"Check if a business accepts online bookings","description":"Returns whether the business has booking enabled (active plan, calendar, at least one service, and site widget shown).","parameters":[{"name":"businessId","in":"query","required":false,"schema":{"type":"integer"},"description":"Optional. Server resolves the business from the request Host header. If provided, the host-resolved value must match or the request is rejected with 400."}],"responses":{"200":{"description":"Availability status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityResponse"},"examples":{"available":{"summary":"Bookings available","value":{"available":true,"hasCalendar":true,"hasServices":true,"calendarId":123,"timezone":"America/Los_Angeles","minimumLeadTimeMinutes":60,"maximumAdvanceTimeMinutes":43200,"bookingWidgetUrl":"https://example.com/?booking=open"}}}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"businessId must be a positive integer","value":{"error":"businessId must be a positive integer"}}}}}},"404":{"description":"Business, service, or booking resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Not found","value":{"error":"Not found"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Internal server error","value":{"error":"Internal server error"}}}}}}}}},"/api/booking/services":{"get":{"tags":["Booking"],"operationId":"listBookingServices","summary":"List available services for a business","parameters":[{"name":"businessId","in":"query","required":false,"schema":{"type":"integer"},"description":"Optional. Server resolves the business from the request Host header. If provided, the host-resolved value must match or the request is rejected with 400."}],"responses":{"200":{"description":"List of services","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicesResponse"},"examples":{"services":{"summary":"Service list","value":{"services":[{"id":456,"name":"Consultation","description":"Initial consultation.","durationMinutes":60,"duration":"1 hour","price":0,"startIncrement":15,"bufferTime":0,"preBookingQuestions":[{"question":"What would you like help with?","answerType":"multiple-lines","required":true}],"phoneNumberRequired":true,"location":{"type":"google_meet"},"requiresPayment":false,"bookingUrl":"https://example.com/?booking=open&service=456"}],"bookingWidgetUrl":"https://example.com/?booking=open","currency":"USD"}}}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"businessId must be a positive integer","value":{"error":"businessId must be a positive integer"}}}}}},"404":{"description":"Business, service, or booking resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Not found","value":{"error":"Not found"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Internal server error","value":{"error":"Internal server error"}}}}}}}}},"/api/booking/time-slots":{"get":{"tags":["Booking"],"operationId":"getBookingTimeSlots","summary":"Get available time slots for a date and service","description":"Times are returned as UTC ISO strings. The `date` parameter is interpreted as a local day in `customerTimezone` when provided; otherwise it is interpreted in the business calendar timezone.","parameters":[{"name":"businessId","in":"query","required":false,"schema":{"type":"integer"},"description":"Optional. Server resolves the business from the request Host header. If provided, the host-resolved value must match or the request is rejected with 400."},{"name":"date","in":"query","required":true,"schema":{"type":"string","format":"date","example":"2026-05-15"},"description":"ISO date (YYYY-MM-DD). With `customerTimezone`, this is the customer's local day. Without `customerTimezone`, this is the business/calendar local day."},{"name":"serviceId","in":"query","required":true,"schema":{"type":"integer"},"description":"The service ID from /api/booking/services."},{"name":"customerTimezone","in":"query","required":false,"schema":{"type":"string","example":"America/Los_Angeles"},"description":"IANA timezone. When provided, only slots within the requested date in this customer-local timezone are returned. When omitted, slots are filtered by the business/calendar timezone."}],"responses":{"200":{"description":"Available time slots","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeSlotsResponse"},"examples":{"slots":{"summary":"Available slots","value":{"slots":[{"id":"2026-05-15T16:00:00.000Z","startTime":"2026-05-15T16:00:00.000Z","endTime":"2026-05-15T17:00:00.000Z","available":true,"bookingUrl":"https://example.com/?booking=open&service=456&date=2026-05-15&slot=2026-05-15T16%3A00%3A00.000Z"}],"timezone":"America/Los_Angeles"}}}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"businessId must be a positive integer","value":{"error":"businessId must be a positive integer"}}}}}},"404":{"description":"Business, service, or booking resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Not found","value":{"error":"Not found"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"error":{"summary":"Internal server error","value":{"error":"Internal server error"}}}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"AvailabilityResponse":{"type":"object","required":["available","hasCalendar","hasServices"],"properties":{"available":{"type":"boolean","description":"True when the business can accept bookings via the website widget right now (plan + calendar + services + widget shown)."},"hasCalendar":{"type":"boolean"},"hasServices":{"type":"boolean"},"calendarId":{"type":["integer","null"]},"timezone":{"type":["string","null"]},"isDemoMode":{"type":"boolean"},"minimumLeadTimeMinutes":{"type":"integer"},"maximumAdvanceTimeMinutes":{"type":"integer"},"bookingWidgetUrl":{"type":"string","format":"uri","description":"Pre-built deep link that opens the booking widget (?booking=open). Use this as the booking entry point to return to users."}}},"ServicesResponse":{"type":"object","required":["services"],"properties":{"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"isDemoMode":{"type":"boolean"},"bookingWidgetUrl":{"type":"string","format":"uri","description":"Pre-built deep link that opens the booking widget with no service pre-selected (?booking=open)."},"currency":{"type":"string","description":"ISO 4217 currency code from the business's invoicing settings.","example":"USD"}}},"Service":{"type":"object","required":["id","name","description","durationMinutes","duration","price","startIncrement","bufferTime","preBookingQuestions","requiresPayment"],"properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string","description":"May be an empty string."},"durationMinutes":{"type":"integer"},"duration":{"type":"string","description":"Formatted duration string, e.g. '1 hour'."},"price":{"type":"number","description":"0 when the service has no set price."},"startIncrement":{"type":"integer","description":"Slot start interval in minutes (e.g. 15 = slots offered on every quarter hour)."},"bufferTime":{"type":"integer","description":"Buffer minutes appended after each booking."},"preBookingQuestions":{"type":"array","description":"Custom questions the business wants the customer to answer before booking. Agents can encode answers in the deep link `answers` query param, but the customer still reviews and confirms in the widget.","items":{"$ref":"#/components/schemas/PreBookingQuestion"}},"phoneNumberRequired":{"type":"boolean","description":"When true, the booking widget form requires the customer phone number before confirmation."},"location":{"$ref":"#/components/schemas/ServiceLocation"},"bookingUrl":{"type":"string","format":"uri","description":"Pre-built deep link that opens the widget with this service pre-selected (?booking=open&service={id}). Return this URL to users to let them book this service."},"requiresPayment":{"type":"boolean","description":"True when picking this service will surface a payment step before confirmation."},"cancellationPolicy":{"type":"object","additionalProperties":true,"description":"Cancellation policy details for the service, when configured."}}},"PreBookingQuestion":{"type":"object","required":["question","answerType"],"properties":{"question":{"type":"string"},"answerType":{"type":"string","enum":["one-line","multiple-lines","radio","checkbox","dropdown"]},"required":{"type":"boolean"},"options":{"type":"array","description":"Provided for radio / checkbox / dropdown answer types.","items":{"$ref":"#/components/schemas/PreBookingOption"}}}},"PreBookingOption":{"type":"object","required":["id","label"],"properties":{"id":{"type":"string"},"label":{"type":"string"}}},"ServiceLocation":{"description":"How the booking will be conducted. The `type` field discriminates the variant. May be omitted when no location has been configured.","oneOf":[{"$ref":"#/components/schemas/ProvidedPhoneLocation"},{"$ref":"#/components/schemas/AskPhoneLocation"},{"$ref":"#/components/schemas/ProvidedAddressLocation"},{"$ref":"#/components/schemas/AskAddressLocation"},{"$ref":"#/components/schemas/GoogleMeetLocation"}],"discriminator":{"propertyName":"type","mapping":{"provided_phone":"#/components/schemas/ProvidedPhoneLocation","ask_phone":"#/components/schemas/AskPhoneLocation","provided_address":"#/components/schemas/ProvidedAddressLocation","ask_address":"#/components/schemas/AskAddressLocation","google_meet":"#/components/schemas/GoogleMeetLocation"}}},"ProvidedPhoneLocation":{"type":"object","required":["type","phoneNumber"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"provided_phone"},"phoneNumber":{"type":"string"}}},"AskPhoneLocation":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"ask_phone"}}},"ProvidedAddressLocation":{"type":"object","required":["type","address"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"provided_address"},"address":{"type":"string"}}},"AskAddressLocation":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"ask_address"}}},"GoogleMeetLocation":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"type":"string","const":"google_meet"}}},"TimeSlotsResponse":{"type":"object","required":["slots"],"properties":{"slots":{"type":"array","items":{"$ref":"#/components/schemas/TimeSlot"}},"timezone":{"type":"string","description":"IANA timezone of the calendar the slots were calculated in."},"message":{"type":"string"},"isDemoMode":{"type":"boolean"}}},"TimeSlot":{"type":"object","required":["id","startTime","endTime","available"],"properties":{"id":{"type":"string"},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"available":{"type":"boolean"},"bookingUrl":{"type":"string","format":"uri","description":"Pre-built deep link for this specific slot (?booking=open&service={id}&date={date}&slot={startTime}). Only present on available slots."}}}}}}