{"openapi":"3.1.0","info":{"title":"BKlayer Agent API","version":"1.0.0","description":"JSON API for ChatGPT Actions and external integrations to find BKlayer businesses, check appointment availability, and create bookings. Public discovery and business-scoped booking endpoints do not require authentication. Some external endpoints require X-API-Key. Dashboard endpoints require a Supabase user session and are not intended for external Actions."},"servers":[{"url":"https://bklayer.com"}],"paths":{"/api/v1/businesses":{"get":{"operationId":"searchPublicBusinesses","summary":"Search public businesses by name, category, or city","description":"Use this first when the user asks for a business without providing a slug, for example \"buscame un fisio en Alcorcon\". The q parameter searches name, slug, city, and category. Business-name search is accent-insensitive, case-insensitive, and tolerant of spaces/hyphens, so \"Estética Avanzada\", \"Estetica Avanzada\", \"estetica avanzada\", and \"estetica-avanzada\" should resolve the same business. Use category and city to narrow discovery by service type and location. If an exact business-name search does not find the expected business, retry with broader category/type/city terms before telling the user it is unavailable. Results only include active businesses with a slug; use that slug for services, availability, and bookings.","security":[],"parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Free-text search matched case-insensitively against business name, slug, city, and category."},{"name":"category","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Case-insensitive category filter, such as fisioterapia or peluqueria. Use this when the user describes the type of business they need."},{"name":"city","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":100},"description":"Case-insensitive city filter, such as Alcorcon or Madrid. Use this when the user gives a location."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Maximum number of businesses to return."},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0},"description":"Pagination offset."}],"responses":{"200":{"description":"Business search results","content":{"application/json":{"schema":{"type":"object","required":["data","limit","offset"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Business"}},"limit":{"type":"integer"},"offset":{"type":"integer"}}},"examples":{"peluqueriaSearch":{"summary":"Search result for a hair salon","value":{"data":[{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Peluqueria Loli","slug":"peluqueria-loli","category":"peluqueria","city":"Madrid","description":null,"timezone":"Europe/Madrid","business_type":"appointment","requires_party_size":false,"default_turn_duration_minutes":null,"capacity_mode":null,"max_party_size_auto_confirm":null}],"limit":10,"offset":0}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}":{"get":{"operationId":"getBusiness","summary":"Get one public business with active services","description":"Returns one active business identified by UUID or slug, plus its active services. Use this after search when an agent needs to confirm the business and available services in one call.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"responses":{"200":{"description":"Business detail with active services","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessDetailResponse"},"examples":{"businessDetail":{"summary":"Business detail with active services","value":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Peluqueria Loli","slug":"peluqueria-loli","category":"peluqueria","city":"Madrid","timezone":"Europe/Madrid","business_type":"appointment","requires_party_size":false,"default_turn_duration_minutes":null,"capacity_mode":null,"max_party_size_auto_confirm":null,"meal_periods":null,"services":[{"id":"528c1428-36cb-46ba-b533-86e1f7b972ff","name":"Mechas","duration_min":60,"price_cents":5000,"currency":"EUR"}]}}}}}},"401":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/ai/booking-intent":{"post":{"operationId":"resolveBookingIntent","summary":"Resolve or confirm a booking from natural language","description":"Rule-based booking orchestrator for simple Spanish booking intents. Use it when the user says something like \"Quiero fisio mañana por la tarde en Alcorcon\". With confirm=false it returns a proposal and suggested slots without creating a booking. Because confirm=true creates a real booking with external side effects, agents must only send confirm=true after showing a complete summary and receiving explicit user confirmation after that summary. Providing missing data, choosing a time, or saying \"sin preferencia\" is not confirmation.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingIntentRequest"},"examples":{"fisioIntent":{"summary":"Ask for physiotherapy tomorrow afternoon","value":{"intent":"Quiero fisio mañana por la tarde en Alcorcón","customer":{"name":"Gabriel Flores","email":"admin@bklayer.com","phone":"630630630"},"confirm":false}}}}}},"responses":{"200":{"description":"Booking proposal with suggested slots, or a need_service response when the business is clear but the service must be chosen by the user.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/BookingIntentProposalResponse"},{"$ref":"#/components/schemas/BookingIntentNeedServiceResponse"}]},"examples":{"proposal":{"summary":"Suggested booking proposal","value":{"status":"proposal","business":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Fisio Centro","slug":"fisio-centro","category":"fisioterapia","city":"Alcorcón","description":null,"timezone":"Europe/Madrid"},"service":{"id":"528c1428-36cb-46ba-b533-86e1f7b972ff","name":"Consulta de fisioterapia","description":null,"duration_min":60,"price_cents":5000,"currency":"EUR","buffer_after_min":0},"date":"2026-04-30","time_preference":"afternoon","suggested_slots":[{"starts_at":"2026-04-30T14:30:00.000Z","ends_at":"2026-04-30T15:30:00.000Z","local_time":"16:30","available":true}]}},"needService":{"summary":"Business found but service must be selected","value":{"status":"need_service","business":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"OrigenKinesis","slug":"origenkinesis","category":"fisioterapia","city":"Alcorcón","description":null,"timezone":"Europe/Madrid"},"services":[{"id":"dd109448-994b-4cdd-9847-6516f6f68ef5","name":"Masaje Descontracturante","description":null,"duration_min":60,"price_cents":5000,"currency":"EUR","buffer_after_min":0},{"id":"528c1428-36cb-46ba-b533-86e1f7b972ff","name":"Masaje Relajante","description":null,"duration_min":60,"price_cents":5000,"currency":"EUR","buffer_after_min":0}],"message":"He encontrado OrigenKinesis en Alcorcón. Estos son sus servicios disponibles: Masaje Descontracturante y Masaje Relajante. ¿Cuál quieres reservar?"}}}}}},"201":{"description":"Booking confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingIntentConfirmedResponse"}}}},"400":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/services":{"get":{"operationId":"listBusinessServices","summary":"List active services for a business","description":"Use this after finding a business and before checking availability when the user has not provided an exact service id. Returns only active services for an active business.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"responses":{"200":{"description":"Active services for the requested business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListServicesResponse"},"examples":{"activeServices":{"summary":"Active services for a hair salon","value":{"business":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Peluqueria Loli","slug":"peluqueria-loli"},"services":[{"id":"dd109448-994b-4cdd-9847-6516f6f68ef5","name":"Corte Adulto","description":null,"duration_min":60,"price_cents":0,"currency":"EUR","buffer_after_min":0},{"id":"528c1428-36cb-46ba-b533-86e1f7b972ff","name":"Mechas","description":null,"duration_min":30,"price_cents":5000,"currency":"EUR","buffer_after_min":0}]}}}}}},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/staff":{"get":{"operationId":"listBusinessStaff","summary":"List active staff for a business","description":"Public endpoint that returns active staff members for an active business. Use staff_id from this response when the user chooses a specific professional.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"responses":{"200":{"description":"Active staff for the requested business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListStaffResponse"},"examples":{"activeStaff":{"summary":"Active staff for a business","value":{"business":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Peluqueria Loli","slug":"peluqueria-loli"},"staff":[{"id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","name":"Laura","role":"Estilista","active":true}]}}}}}},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/zones":{"get":{"operationId":"listBusinessRestaurantZones","summary":"List active restaurant zones for a business","description":"Returns active restaurant zones configured for a business. Use this to resolve user-facing zone names/slugs before availability checks. In zones_preference mode, zones are preferences and must not be presented as guaranteed capacity. In zones_capacity mode, zone capacity is validated as real availability.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"responses":{"200":{"description":"Active restaurant zones","content":{"application/json":{"schema":{"type":"object","required":["business","zones"],"properties":{"business":{"type":"object","required":["id","name","slug","business_type","zone_selection_mode"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"business_type":{"$ref":"#/components/schemas/BusinessType"},"capacity_mode":{"$ref":"#/components/schemas/CapacityMode"},"zone_selection_mode":{"$ref":"#/components/schemas/ZoneSelectionMode"}}},"zones":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/RestaurantZoneSummary"},{"type":"object","required":["is_active"],"properties":{"is_active":{"type":"boolean"}}}]}}}},"examples":{"restaurantZones":{"summary":"Active zones for a restaurant","value":{"business":{"id":"3f3c7a59-3f6f-4b88-bb5e-3f6b59f75c21","name":"Restaurante X","slug":"restaurante-x","business_type":"restaurant","capacity_mode":"zones_capacity","zone_selection_mode":"capacity"},"zones":[{"id":"73f9f7f5-49ab-4f9b-9d4d-cdbfbd69e7a1","slug":"interior","name":"Interior","sort_order":0,"is_active":true},{"id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","slug":"terraza","name":"Terraza","sort_order":1,"is_active":true}]}}}}}},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/availability":{"get":{"operationId":"checkBusinessAvailability","summary":"Check available appointment or restaurant slots","description":"Returns bookable slots for one business on one date. Never invent availability. Agents must only offer slots returned by this endpoint where available=true, and must never offer slots with available=false as available alternatives. If the user requested a specific local time, first find a slot whose local_time exactly matches that time: if it exists and available=true, offer that exact slot; if it exists and available=false, explain it is not available and offer only other available=true slots; if it does not exist, say that time is not offered and list only available=true alternatives. If the user chooses a concrete time, the pre-booking summary must keep that exact local_time and createBusinessBooking must use the starts_at from that exact slot. Do not silently change 17:00 into 19:00 or any other time. If a different time is proposed or selected, explicitly tell the user the time changed and ask for a new confirmation for the new time. Appointment businesses require service_id from listBusinessServices and optionally accept staff_id. For appointment slots, inspect available_staff_ids/available_staff_count: if 0 do not offer the slot; if exactly 1, autoselect that staff UUID, resolve and show the real staff name in the summary, and send the real staff_id to create; do not show \"Sin preferencia\" as the final professional for a single-staff slot; if 2 or more, ask the user to choose a professional or explicitly choose \"sin preferencia\". When 2+ staff are available and the user says \"me da igual\", \"cualquiera\", or \"sin preferencia\", prefer assigning a concrete staff UUID before the summary: use selected_staff_id only if it belongs to available_staff_ids, otherwise choose one available_staff_ids value, resolve the name, show it in the summary, and send that real staff_id. Never use text such as \"sin preferencia\" or a staff name such as \"Sandra\" as staff_id. If the final professional changes, show a new complete summary and ask for fresh confirmation. selected_staff_id is only a backend default/suggestion and MUST NOT be used to skip that choice when more than one professional is available unless the user chose \"sin preferencia\" and selected_staff_id belongs to available_staff_ids. After any successful createBusinessBooking or createBooking, previous availability for the same business/service/date/time/staff is stale and agents must call checkBusinessAvailability again before offering, confirming, or creating another booking in that slot. Restaurant businesses require party_size and meal_period unless checking a concrete starts_at/time where the meal period can be inferred; staff_id is ignored for restaurants in V1. Restaurant zone inputs are optional and additive: restaurant_zone_id, restaurant_zone_slug, and restaurant_zone_preference. In simple mode, capacity is global. In zones_preference mode, zone is a preference and not a guarantee. In zones_capacity mode, zone availability is validated as real capacity. Pass the exact starts_at value from an available=true slot to create a booking.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."},{"name":"service_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Service UUID returned by the public business page or known by the agent. Required for appointment businesses. For restaurants, pass the generic \"Reserva de mesa\" service when known."},{"name":"staff_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Optional staff member UUID returned by listBusinessStaff. Used by appointments and ignored for restaurants in V1."},{"name":"party_size","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Required for restaurant availability. Number of diners."},{"name":"meal_period","in":"query","required":false,"schema":{"$ref":"#/components/schemas/MealPeriod"},"description":"Required for restaurant availability unless starts_at/time allows the period to be inferred."},{"name":"restaurant_zone_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Optional active restaurant zone UUID for restaurant checks. Must belong to the same business."},{"name":"restaurant_zone_slug","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":120},"description":"Optional active restaurant zone slug. APIs may resolve this to restaurant_zone_id before validation."},{"name":"restaurant_zone_preference","in":"query","required":false,"schema":{"type":"string","enum":["zone","no_preference"]},"description":"Optional zone preference. Use zone for a concrete zone request and no_preference when any zone is acceptable."},{"name":"time","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{2}:\\d{2}$","examples":["21:00"]},"description":"Optional local HH:mm time for a concrete restaurant availability check."},{"name":"starts_at","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Optional exact datetime for a concrete restaurant availability check."},{"name":"date","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","examples":["2026-05-07"]},"description":"Date in YYYY-MM-DD format in the business timezone."}],"responses":{"200":{"description":"Availability for the requested service and date","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityResponse"},"examples":{"availableSlots":{"summary":"Availability with available and occupied slots","value":{"business":{"id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","name":"Peluqueria Loli","slug":"peluqueria-loli"},"service":{"id":"528c1428-36cb-46ba-b533-86e1f7b972ff","name":"Mechas","duration_min":30},"date":"2026-05-07","timezone":"Europe/Madrid","staff_id":null,"slots":[{"starts_at":"2026-05-07T07:00:00.000Z","ends_at":"2026-05-07T07:30:00.000Z","local_time":"09:00","available":true,"available_staff_ids":["a4fe7192-0b5b-4d15-9de2-96f3b4478f11","bb18cf95-e83a-4b8c-9736-19dbd39a60ef"],"selected_staff_id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","available_staff_count":2,"staff_capacity":2},{"starts_at":"2026-05-07T14:30:00.000Z","ends_at":"2026-05-07T15:00:00.000Z","local_time":"16:30","available":false,"available_staff_ids":[],"selected_staff_id":null,"available_staff_count":0,"staff_capacity":2},{"starts_at":"2026-05-07T15:30:00.000Z","ends_at":"2026-05-07T16:00:00.000Z","local_time":"17:30","available":true,"available_staff_ids":["bb18cf95-e83a-4b8c-9736-19dbd39a60ef"],"selected_staff_id":"bb18cf95-e83a-4b8c-9736-19dbd39a60ef","available_staff_count":1,"staff_capacity":2}]}},"restaurantAvailability":{"summary":"Restaurant dinner availability for four diners","value":{"business":{"id":"3f3c7a59-3f6f-4b88-bb5e-3f6b59f75c21","name":"Restaurante X","slug":"restaurante-x"},"date":"2026-05-09","timezone":"Europe/Madrid","capacity_mode":"zones_capacity","zone_selection_mode":"capacity","meal_period":"dinner","party_size":4,"requested_zone":{"slug":"terraza","name":"Terraza","preference":"zone"},"requested_zone_available":true,"available_zones":[{"id":"73f9f7f5-49ab-4f9b-9d4d-cdbfbd69e7a1","slug":"interior","name":"Interior","sort_order":0},{"id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","slug":"terraza","name":"Terraza","sort_order":1}],"zone_alternatives":[],"agent_instructions":"Zone selection is part of real availability. If a requested zone is full, offer only real alternatives returned by availability.","staff_id":null,"service_id":"45e1c8ac-8b7d-4d4d-a9d8-2c50b8c72440","available":true,"active_zones":[{"id":"73f9f7f5-49ab-4f9b-9d4d-cdbfbd69e7a1","name":"Interior","slug":"interior","capacity":30,"sort_order":0},{"id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","name":"Terraza","slug":"terraza","capacity":12,"sort_order":1}],"slots":[{"starts_at":"2026-05-09T19:00:00.000Z","ends_at":"2026-05-09T20:30:00.000Z","local_time":"21:00","local_time_label":"21:00","available":true,"remaining_capacity":6,"slot_capacity":10,"party_size":4,"meal_period":"dinner","turn_duration_minutes":90,"restaurant_zone_id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","restaurant_zone_name":"Terraza","restaurant_zone_slug":"terraza","restaurant_zone_preference":"zone"}]}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/availability/next":{"get":{"operationId":"getNextBusinessAvailability","summary":"Get the next available appointment slot","description":"Returns the first available slot for one active service, starting from today in the business timezone. The returned slot uses local datetimes with explicit timezone offset for agent compatibility.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."},{"name":"service_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"},"description":"Active service UUID."},{"name":"staff_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Optional staff member UUID returned by listBusinessStaff. When provided, the next slot is searched for that professional."}],"responses":{"200":{"description":"Next available slot, or null when none is found in the search window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NextAvailabilityResponse"},"examples":{"nextSlot":{"summary":"Next available slot with timezone offset","value":{"business_id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","service_id":"528c1428-36cb-46ba-b533-86e1f7b972ff","staff_id":null,"timezone":"Europe/Madrid","next_slot":{"start_time":"2026-05-10T17:00:00+02:00","end_time":"2026-05-10T18:00:00+02:00"}}},"noSlot":{"summary":"No available slot","value":{"business_id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","service_id":"528c1428-36cb-46ba-b533-86e1f7b972ff","staff_id":null,"timezone":"Europe/Madrid","next_slot":null}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"401":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/bookings":{"post":{"operationId":"createBooking","summary":"Create a booking with the global v1 booking contract","description":"Creates a real booking with external side effects using business_id, service_id, and start_time with timezone offset. This API-key contract is the preferred standard endpoint for external agents. Agents must call it only after checkBusinessAvailability returned available=true for the slot, after showing a complete summary, and after receiving explicit user confirmation after that summary. Providing missing name/phone/email, choosing a time, or choosing \"sin preferencia\" is not confirmation. After success, previous availability for the same business/service/date/time/staff is stale and must be checked again before another booking in that slot. The same route also has a dashboard-only starts_at contract that requires a Supabase session and is not intended for Actions.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGlobalBookingRequest"},"examples":{"globalBookingRequest":{"summary":"Create a global booking with offset datetime","value":{"business_id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","service_id":"528c1428-36cb-46ba-b533-86e1f7b972ff","staff_id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","start_time":"2026-05-10T17:00:00+02:00","customer":{"name":"Jane Smith","email":"jane@example.com","phone":"+34630630630"},"notes":"Primera visita","agent_id":"claude","idempotency_key":"optional-key"}},"restaurantGlobalBookingRequest":{"summary":"Create a restaurant table booking","value":{"business_id":"3f3c7a59-3f6f-4b88-bb5e-3f6b59f75c21","service_id":"45e1c8ac-8b7d-4d4d-a9d8-2c50b8c72440","start_time":"2026-05-09T21:00:00+02:00","customer":{"name":"Gabriel","phone":"600000000"},"party_size":4,"meal_period":"dinner"}}}}}},"responses":{"200":{"description":"Existing booking returned for a repeated idempotency_key. email_status is skipped in this response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGlobalBookingResponse"}}}},"201":{"description":"Booking created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGlobalBookingResponse"},"examples":{"bookingCreated":{"summary":"Booking created successfully","value":{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_reference":"BK-2026-ABCD","business_id":"982eb5d6-e4a9-4c17-ad40-80ff89baec5e","business_name":"Peluqueria Loli","service_name":"Mechas","customer":{"name":"Jane Smith","email":"jane@example.com","phone":"+34630630630"},"start_time":"2026-05-10T17:00:00+02:00","end_time":"2026-05-10T18:00:00+02:00","status":"confirmed","price":5000,"currency":"EUR","email_status":"sent","calendar_status":"created"}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"401":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/bookings":{"post":{"operationId":"createBusinessBooking","summary":"Create a booking from a selected slot","description":"Creates a real booking with external side effects for the business identified by UUID or slug. Call only after showing a complete summary and receiving explicit user confirmation after that summary. If the user chose a concrete local_time such as 17:00, the summary must show exactly 17:00 and this operation must use the starts_at from the slot whose local_time is exactly 17:00. Confirming a booking for a different time is invalid unless the agent explicitly warned that the time changed and obtained a fresh post-summary confirmation for the new time. Never silently change \"A las 17:00 está bien\" into 19:00. The request contract requires confirmation_summary_presented=true, confirmation_obtained=true, and user_confirmation_text from the post-summary confirmation. Providing missing name/phone/email is not confirmation. Choosing a time is not confirmation. Saying \"me da igual\", \"cualquiera\", or \"sin preferencia\" for professional is not confirmation. Saying \"reserva a nombre de X\" while giving initial data is not confirmation if the summary has not already been shown. If the user gives time + customer data + \"sin preferencia\" in one message, treat it as slot/staff/data collection only: locate the exact local_time slot, assign staff according to available_staff_count, show a summary with the same local_time and final staff name, then ask for confirmation. A second booking, even \"igual que la anterior\", still needs its own complete summary and explicit confirmation. The starts_at value should come from an available=true slot returned by checkBusinessAvailability, and service_id must come from listBusinessServices. For appointments: if available_staff_count is 0, do not create; if available_staff_count is exactly 1, autoselect the only available_staff_ids value, resolve and show the real staff name in the summary, and send that real staff_id; do not show \"Sin preferencia\" as the final professional; if available_staff_count is 2+ and the user chose \"sin preferencia\", prefer assigning a concrete staff_id before confirmation, using selected_staff_id only when it belongs to available_staff_ids, otherwise choosing one available_staff_ids value, then show the real assigned staff name and send the real staff_id. Claude/MCP should not leave final appointment creation at \"sin preferencia\": assign a concrete available staff_id before confirmation, show the real assigned staff name, and send that real staff_id. Never send \"sin preferencia\", \"Sandra\", or any staff name as staff_id. If the final professional changes between summary and create, the previous confirmation is invalid; show a new complete summary and get fresh confirmation. After success, all previous availability for the same business/service/date/time/staff is stale; call checkBusinessAvailability again before another booking in that slot. If creation fails because the slot or staff is unavailable, do not invent alternatives; call checkBusinessAvailability before proposing real alternatives. For restaurants, send service_id, starts_at, party_size, and meal_period; turn_duration_minutes is optional; never send staff_id. Restaurant zones are structured via restaurant_zone_id, restaurant_zone_slug, and restaurant_zone_preference (zone or no_preference). Do not send restaurant_zone_name_snapshot; snapshot is server-generated. In simple and zones_preference, zone is preference context and must not be presented as guaranteed capacity. In zones_capacity with active zones, create requires explicit zone intent: restaurant_zone_preference=no_preference or preference=zone with a valid active zone id/slug. If the requested zone is unavailable, do not force create and call checkBusinessAvailability for real alternatives.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookingRequest"},"examples":{"bookingRequest":{"summary":"Create a booking with explicit service and slot","value":{"service_id":"528c1428-36cb-46ba-b533-86e1f7b972ff","staff_id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","starts_at":"2026-05-07T07:00:00.000Z","confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"confirmo","customer":{"name":"Ana Garcia","email":"ana@example.com","phone":"600000000"}}},"naturalBookingRequest":{"summary":"Create a booking with natural agent inputs","value":{"service_name":"Mechas","date":"2026-05-07","time_preference":"morning","confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"adelante","customer":{"name":"Ana Garcia","email":"ana@example.com","phone":"600000000"}}},"restaurantBookingRequest":{"summary":"Create a restaurant table booking","value":{"service_id":"45e1c8ac-8b7d-4d4d-a9d8-2c50b8c72440","starts_at":"2026-05-09T21:00:00+02:00","confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"sí, confirmo","customer":{"name":"Gabriel","phone":"600000000"},"party_size":4,"meal_period":"dinner","restaurant_zone_slug":"terraza","restaurant_zone_preference":"zone"}}}}}},"responses":{"201":{"description":"Booking created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookingResponse"},"examples":{"bookingCreated":{"summary":"Booking created successfully","value":{"success":true,"booking_id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_reference":"BK-2026-ABCD","email_status":"sent","calendar_status":"created","starts_at":"2026-05-07T07:00:00+00:00","local_time":"09:00"}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"description":"No matching availability or selected slot unavailable. The response may include alternatives, but agents must treat their prior availability as stale and call checkBusinessAvailability again before offering, confirming, or creating a replacement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoAvailabilityResponse"},"examples":{"noAvailability":{"summary":"Closest alternatives for the requested time","value":{"error":"No availability","code":"NO_AVAILABILITY","alternatives":[{"starts_at":"2026-05-07T14:30:00.000Z","ends_at":"2026-05-07T15:00:00.000Z","local_time":"16:30","available":true},{"starts_at":"2026-05-07T16:00:00.000Z","ends_at":"2026-05-07T16:30:00.000Z","local_time":"18:00","available":true},{"starts_at":"2026-05-07T16:30:00.000Z","ends_at":"2026-05-07T17:00:00.000Z","local_time":"18:30","available":true}]}}}}}}}}},"/api/v1/businesses/{idOrSlug}/bookings/search":{"get":{"operationId":"searchCustomerBookings","summary":"Search verified customer bookings for a business","description":"Public agent-safe booking lookup endpoint. Phone is required. Without verification_code, it only returns a count and a message requiring OTP verification. With a valid OTP, it returns bookings from the selected business whose snapshot phone or customer phone matches, prioritizing active bookings first and exposing history separately so cancelled records do not hide active reservations.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."},{"name":"phone","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":30},"description":"Customer phone used to verify the booking search."},{"name":"verification_code","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{6}$"},"description":"Six-digit OTP sent to the customer email. Required to return booking details."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["upcoming","past","cancelled","all"],"default":"upcoming"},"description":"Booking status window. upcoming returns confirmed/rescheduled bookings from now onward."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":10}}],"responses":{"200":{"description":"Verified customer bookings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchCustomerBookingsResponse"},"examples":{"needsVerification":{"summary":"Phone matched but details require additional verification","value":{"requires_verification":true,"verification_method":"email_otp","next_action":"request_customer_booking_verification_code","count":2,"message":"Para ver los detalles, solicita un código de verificación enviado al email asociado."}},"customerBookings":{"summary":"Upcoming customer bookings after additional verification","value":{"bookings":[{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_reference":"BK-2026-ABCD","business_name":"Peluqueria Loli","business_ref":"peluqueria-loli","business_type":"appointment","customer_name":"Ana Garcia","service_name":"Mechas","staff_name":"Laura","starts_at":"2026-05-07T07:00:00+00:00","ends_at":"2026-05-07T07:30:00+00:00","created_at":"2026-05-01T10:00:00+00:00","updated_at":"2026-05-01T10:00:00+00:00","status":"confirmed","party_size":4,"meal_period":"dinner","restaurant_zone_id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","restaurant_zone_name_snapshot":"Terraza","restaurant_zone_slug":"terraza","restaurant_zone_preference":"zone","notes":"Mesa tranquila, por favor."}],"active_bookings":[{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_reference":"BK-2026-ABCD","business_name":"Peluqueria Loli","business_ref":"peluqueria-loli","business_type":"appointment","customer_name":"Ana Garcia","service_name":"Mechas","staff_name":"Laura","starts_at":"2026-05-07T07:00:00+00:00","ends_at":"2026-05-07T07:30:00+00:00","created_at":"2026-05-01T10:00:00+00:00","updated_at":"2026-05-01T10:00:00+00:00","status":"confirmed","party_size":4,"meal_period":"dinner","restaurant_zone_id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","restaurant_zone_name_snapshot":"Terraza","restaurant_zone_slug":"terraza","restaurant_zone_preference":"zone","notes":"Mesa tranquila, por favor."}],"history_bookings":[],"cancelled_bookings":[],"has_active_bookings":true,"has_more_results":false,"limit":10,"total_matches":1,"active_total":1,"history_total":0,"cancelled_total":0}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/bookings/search/request-code":{"post":{"operationId":"requestCustomerBookingVerificationCode","summary":"Request OTP for customer booking search","description":"Sends a six-digit OTP to the email associated with bookings matching the provided phone. Does not reveal whether a phone exists when no bookings are found, and only returns a masked email when a code is sent.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCustomerBookingVerificationCodeRequest"},"examples":{"requestCode":{"summary":"Request booking search OTP","value":{"phone":"600000000"}}}}}},"responses":{"200":{"description":"Verification code request accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCustomerBookingVerificationCodeResponse"},"examples":{"codeSent":{"summary":"Code sent to masked email","value":{"status":"code_sent","masked_email":"a***@example.com"}},"emailRequired":{"summary":"Matching bookings have no email","value":{"status":"email_required"}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"429":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/bookings/{bookingId}/reschedule":{"post":{"operationId":"rescheduleBusinessBooking","summary":"Reschedule an existing booking","description":"Real side-effect action for public agents. Use this when the user asks to change, move, or reschedule an existing booking. Requires a valid customer OTP verification_code from requestCustomerBookingVerificationCode plus explicit confirmation after showing the old/new summary. Confirmation never replaces OTP. Phone, email, booking_ref, or \"si, cambiala\" alone do not authorize rescheduling. Do not create a new booking for requests like \"cambiala\", \"muevela\", or \"reprograma\"; use the booking_id from the current conversation when available. For appointments, send staff_id when the user changes professional; BKlayer validates and persists that exact professional and will not silently assign another one. If staff_id is omitted for an appointment, the original staff member is kept. For restaurants, never send staff_id; send the exact starts_at from an available=true restaurant slot and include party_size when the group size changes plus meal_period when changing lunch/comida vs dinner/cena or when the new slot depends on meal period. If the user changes the number of people, party_size is mandatory. Restaurant zone fields are optional and additive: restaurant_zone_id, restaurant_zone_slug, and restaurant_zone_preference (zone or no_preference). Do not send restaurant_zone_name_snapshot; it is generated server-side. If the user requests a zone change (for example Interior, Salón, Terraza, Barra), agents must include restaurant_zone_slug or restaurant_zone_id plus restaurant_zone_preference=zone in this reschedule request. If restaurant_zone_* is omitted, BKlayer keeps the existing zone assignment. Agents must not claim a zone change unless the request included zone fields and the response confirms the applied zone. In zones_preference, zone is preference context only. In zones_capacity, zone availability is real capacity and unavailable zones should be handled by checking alternatives.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."},{"name":"bookingId","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Existing booking id returned by createBusinessBooking."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleBookingRequest"},"examples":{"rescheduleRequest":{"summary":"Move a booking to a new slot","value":{"starts_at":"2026-04-29T16:00:00.000Z","staff_id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","party_size":2,"meal_period":"lunch","restaurant_zone_slug":"terraza","restaurant_zone_preference":"zone","turn_duration_minutes":90,"customer_verification":{"phone":"600000000","verification_code":"123456"},"confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"Si, confirmo el cambio a las 18:00"}}}}}},"responses":{"200":{"description":"Booking rescheduled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleBookingResponse"},"examples":{"bookingRescheduled":{"summary":"Booking rescheduled successfully","value":{"success":true,"booking_id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_ref":"BK-2026-7SND","starts_at":"2026-04-29T16:00:00+00:00","ends_at":"2026-04-29T17:30:00+00:00","local_time":"18:00","status":"rescheduled","staff_id":"a4fe7192-0b5b-4d15-9de2-96f3b4478f11","staff_name":"Sandra","party_size":2,"meal_period":"lunch","turn_duration_minutes":90,"restaurant_zone_id":"89a55f65-bf6c-4d0a-9e49-8e793f3a7800","restaurant_zone_name_snapshot":"Terraza","restaurant_zone_preference":"zone","calendar_status":"updated"}}}}}},"400":{"description":"Validation, BK_VERIFICATION_REQUIRED, or BK_CONFIRMATION_REQUIRED error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"BK_INVALID_VERIFICATION_CODE or forbidden error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"$ref":"#/components/responses/ApiError"},"409":{"description":"Booking cannot be rescheduled or target slot is unavailable","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ApiError"},{"$ref":"#/components/schemas/SlotUnavailableResponse"}]},"examples":{"slotUnavailable":{"summary":"Requested slot is unavailable","value":{"error":"The requested time slot is not available","code":"SLOT_UNAVAILABLE","alternatives":[{"starts_at":"2026-04-29T15:30:00.000Z","ends_at":"2026-04-29T16:00:00.000Z","local_time":"17:30","available":true}]}}}}}},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/businesses/{idOrSlug}/bookings/{bookingId}/cancel":{"post":{"operationId":"cancelBusinessBooking","summary":"Cancel an existing booking with customer verification","description":"Real side-effect action for public agents. Public agent-safe cancellation endpoint. Use only after a valid customer OTP verification_code from requestCustomerBookingVerificationCode and explicit user confirmation after showing the booking to cancel. Confirmation never replaces OTP. Phone, email, booking_ref, or \"si, cancelala\" alone do not authorize cancellation. Cancels only bookings belonging to the selected business. Deletes the Google Calendar event when google_event_id exists.","security":[],"parameters":[{"name":"idOrSlug","in":"path","required":true,"schema":{"type":"string"},"description":"Business UUID or public slug."},{"name":"bookingId","in":"path","required":true,"schema":{"type":"string"},"description":"Existing booking UUID or visible booking reference such as BK-2026-ABCD."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelBookingRequest"},"examples":{"cancelRequest":{"summary":"Cancel a booking with customer verification","value":{"customer_verification":{"phone":"600000000","verification_code":"123456"},"confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"Si, cancelala"}}}}}},"responses":{"200":{"description":"Booking cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingCancelResponse"},"examples":{"bookingCancelled":{"summary":"Booking cancelled successfully","value":{"success":true,"booking_reference":"BK-2026-ABCD","status":"cancelled","data":{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","booking_ref":"BK-2026-ABCD","status":"cancelled"},"calendar_status":"deleted"}}}}}},"400":{"description":"Validation, BK_VERIFICATION_REQUIRED, or BK_CONFIRMATION_REQUIRED error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"403":{"description":"BK_INVALID_VERIFICATION_CODE or forbidden error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"404":{"description":"Booking was not found by UUID or BK reference for this business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"},"examples":{"bookingNotFound":{"summary":"Booking not found","value":{"error":"Booking not found","code":"BOOKING_NOT_FOUND"}}}}}},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/bookings/{id}":{"patch":{"operationId":"updateDashboardBooking","summary":"Update a booking from the dashboard","description":"Dashboard endpoint. Requires an authenticated Supabase session for the business owner. Updates customer details and notes, and updates the Google Calendar event when google_event_id exists. Not intended for external ChatGPT Actions.","security":[{"DashboardSessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Booking UUID."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingUpdateRequest"},"examples":{"updateBooking":{"summary":"Update customer details and notes","value":{"customer_name":"Ana Garcia","customer_email":"ana@example.com","customer_phone":"600000000","notes":"Prefiere cita tranquila."}}}}}},"responses":{"200":{"description":"Booking updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingUpdateResponse"},"examples":{"updated":{"summary":"Booking updated successfully","value":{"success":true,"data":{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","customer_name":"Ana Garcia","customer_email":"ana@example.com","customer_phone":"600000000","notes":"Prefiere cita tranquila."},"event":{"id":"e0efe423-a020-4e50-b7a7-f28a52d05a59","event_type":"booking_updated"},"calendar_status":"updated"}}}}}},"400":{"$ref":"#/components/responses/ApiError"},"401":{"$ref":"#/components/responses/ApiError"},"403":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/bookings/{id}/reschedule":{"post":{"operationId":"reschedulePublicBooking","summary":"Reschedule a booking by public reference or owner session","description":"Public booking endpoint used by WhatsApp/agent flows with customer OTP verification, and also by authenticated dashboard owners. For appointments it reuses the booking service and keeps the original staff member unless staff_id is provided. For restaurants it validates capacity with party_size and meal_period, excludes the original booking from capacity, never uses staff_id, and preserves the existing restaurant zone. This public route does not accept restaurant_zone_id, restaurant_zone_slug, or restaurant_zone_preference; use the business-scoped reschedule endpoint when a structured zone change is supported.","security":[{"DashboardSessionAuth":[]},{}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Booking UUID or public booking reference such as BK-2026-7SND."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicRescheduleBookingRequest"},"examples":{"publicReschedule":{"summary":"Move a public booking without changing restaurant zone","value":{"starts_at":"2026-04-29T16:00:00.000Z","party_size":2,"meal_period":"lunch","turn_duration_minutes":90,"customer_verification":{"phone":"600000000","verification_code":"123456"},"confirmation_summary_presented":true,"confirmation_obtained":true,"user_confirmation_text":"Si, confirmo el cambio a las 18:00"}}}}}},"responses":{"200":{"description":"Booking rescheduled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DashboardRescheduleBookingResponse"}}}},"400":{"$ref":"#/components/responses/ApiError"},"401":{"$ref":"#/components/responses/ApiError"},"403":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/bookings/{id}/cancel":{"post":{"operationId":"cancelDashboardBooking","summary":"Cancel a booking from the dashboard","description":"Dashboard endpoint. Requires an authenticated Supabase session for the business owner. Cancels confirmed or rescheduled bookings and deletes the Google Calendar event when google_event_id exists. Not intended for external ChatGPT Actions.","security":[{"DashboardSessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Booking UUID."}],"responses":{"200":{"description":"Booking cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingCancelResponse"},"examples":{"cancelled":{"summary":"Booking cancelled successfully","value":{"success":true,"data":{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","status":"cancelled"},"calendar_status":"deleted"}}}}}},"401":{"$ref":"#/components/responses/ApiError"},"403":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}},"/api/v1/bookings/{id}/complete":{"post":{"operationId":"completeDashboardBooking","summary":"Complete a booking from the dashboard","description":"Dashboard endpoint. Requires an authenticated Supabase session for the business owner. Marks a confirmed or rescheduled booking as completed. Not intended for external ChatGPT Actions.","security":[{"DashboardSessionAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Booking UUID."}],"responses":{"200":{"description":"Booking completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingCompleteResponse"},"examples":{"completed":{"summary":"Booking completed successfully","value":{"success":true,"data":{"id":"facc075b-f7c1-4c2f-beb8-02332ea5deb9","status":"completed"}}}}}}},"401":{"$ref":"#/components/responses/ApiError"},"403":{"$ref":"#/components/responses/ApiError"},"404":{"$ref":"#/components/responses/ApiError"},"409":{"$ref":"#/components/responses/ApiError"},"500":{"$ref":"#/components/responses/ApiError"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"BKlayer API key for external agents and integrations. Use X-API-Key: bk_live_xxxxxx."},"DashboardSessionAuth":{"type":"apiKey","in":"cookie","name":"sb-access-token","description":"Supabase dashboard session cookies managed by the BKlayer web app. These endpoints are documented for completeness and are not intended for external Actions."}},"responses":{"ApiError":{"description":"API error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"schemas":{"Business":{"type":"object","required":["id","name","slug"],"description":"Public business record. Use slug as the preferred stable identifier in agent calls.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"category":{"type":["string","null"],"description":"Business category used for discovery, for example fisioterapia."},"city":{"type":["string","null"],"description":"Business city used for local discovery."},"description":{"type":["string","null"]},"timezone":{"type":"string","examples":["Europe/Madrid"]},"business_type":{"$ref":"#/components/schemas/BusinessType"},"requires_party_size":{"type":"boolean"},"default_turn_duration_minutes":{"type":["integer","null"],"minimum":1},"capacity_mode":{"$ref":"#/components/schemas/CapacityMode"},"max_party_size_auto_confirm":{"type":["integer","null"],"minimum":1}}},"Service":{"type":"object","required":["id","name","duration_min"],"description":"Bookable service offered by a business.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"]},"duration_min":{"type":"integer","minimum":1},"price_cents":{"type":"integer","minimum":0},"currency":{"type":"string","examples":["EUR"]},"buffer_after_min":{"type":"integer","minimum":0}}},"Staff":{"type":"object","required":["id","name","active"],"description":"Active professional who can be assigned to availability checks and bookings.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"role":{"type":["string","null"]},"active":{"type":"boolean"}}},"BookingStatus":{"type":"string","enum":["confirmed","rescheduled","cancelled","completed"]},"CalendarStatus":{"type":"string","enum":["created","updated","deleted","skipped","failed"]},"CalendarError":{"type":"string","enum":["missing_credentials","missing_tokens","token_refresh_failed","calendar_insert_failed","calendar_update_failed","calendar_delete_failed","unknown"]},"EmailStatus":{"type":"string","enum":["sent","skipped","failed"]},"BusinessType":{"type":"string","enum":["appointment","restaurant"]},"CapacityMode":{"type":["string","null"],"enum":["simple","zones_preference","zones_capacity",null]},"ZoneSelectionMode":{"type":"string","enum":["global","preference","capacity"]},"RestaurantZonePreference":{"type":"string","enum":["zone","no_preference"]},"MealPeriod":{"type":"string","enum":["lunch","dinner"]},"RestaurantZoneSummary":{"type":"object","required":["id","slug","name","sort_order"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"sort_order":{"type":"integer","minimum":0}}},"RestaurantActiveZone":{"type":"object","required":["id","slug","name","sort_order","capacity"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"sort_order":{"type":"integer","minimum":0},"capacity":{"type":"integer","minimum":1}}},"BusinessMealPeriod":{"type":"object","required":["meal_period","day_of_week","open_time","close_time","slot_capacity","turn_duration_minutes","is_active"],"description":"Restaurant meal period configuration. day_of_week uses 0=Sunday and 6=Saturday.","properties":{"meal_period":{"$ref":"#/components/schemas/MealPeriod"},"day_of_week":{"type":"integer","minimum":0,"maximum":6},"open_time":{"type":"string","examples":["20:00"]},"close_time":{"type":"string","examples":["23:00"]},"slot_capacity":{"type":"integer","minimum":1},"turn_duration_minutes":{"type":"integer","minimum":1},"is_active":{"type":"boolean"}}},"BusinessDetailService":{"type":"object","required":["id","name","duration_min","price_cents","currency"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"duration_min":{"type":"integer","minimum":1},"price_cents":{"type":"integer","minimum":0},"currency":{"type":"string","examples":["EUR"]}}},"BusinessDetailResponse":{"type":"object","required":["id","name","slug","timezone","services"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"category":{"type":["string","null"]},"city":{"type":["string","null"]},"timezone":{"type":"string","examples":["Europe/Madrid"]},"business_type":{"$ref":"#/components/schemas/BusinessType"},"requires_party_size":{"type":"boolean"},"default_turn_duration_minutes":{"type":["integer","null"],"minimum":1},"capacity_mode":{"$ref":"#/components/schemas/CapacityMode"},"max_party_size_auto_confirm":{"type":["integer","null"],"minimum":1},"meal_periods":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/BusinessMealPeriod"}},{"type":"null"}],"description":"Restaurant meal periods when business_type=restaurant; null otherwise."},"services":{"type":"array","items":{"$ref":"#/components/schemas/BusinessDetailService"}}}},"ListServicesResponse":{"type":"object","required":["business","services"],"properties":{"business":{"$ref":"#/components/schemas/Business"},"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}},"ListStaffResponse":{"type":"object","required":["business","staff"],"properties":{"business":{"$ref":"#/components/schemas/Business"},"staff":{"type":"array","items":{"$ref":"#/components/schemas/Staff"}}}},"Slot":{"type":"object","required":["starts_at","ends_at","local_time","available"],"description":"Availability slot returned by check availability. Never invent slots outside this response. Agents must only offer/book slots where available=true; available=false means the slot is disabled/unavailable and must not be offered as an available alternative. For exact-time user requests, match the requested time against local_time exactly. Appointment slots may include available_staff_ids, selected_staff_id, available_staff_count, and staff_capacity; MCP availability may also enrich slots with selected_staff_name, available_staff, staff_selection_required, staff_assignment_note, and customer_facing_staff_phrase after resolving listBusinessStaff. If available_staff_ids has zero values or available_staff_count is 0, do not offer the slot; if it has one value, selected_staff_id must be the only available UUID and the agent must autoselect that professional, resolve/show the real name, and send that staff_id; if it has more than one value, agents must ask the user to choose a professional or \"sin preferencia\". selected_staff_id is only a backend default and must not skip the user choice except after explicit \"sin preferencia\" and only when it belongs to available_staff_ids. Technical field names can guide the agent but must not appear in customer-facing replies: do not say selected_staff_id, available_staff_ids, staff_id, local_time, MCP, schema, payload, UUID, or tool. Say \"Como te da igual, te asigno a Paloma\" or \"Te asigno una profesional disponible: Paloma.\" Restaurant slots may include capacity metadata and never use staff_id.","properties":{"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"},"local_time":{"type":"string","examples":["16:30"],"description":"Local HH:mm time in the business timezone. Use this exact string to match user-requested times such as 16:00."},"local_time_label":{"type":"string","examples":["16:30"]},"available":{"type":"boolean","description":"Only true slots may be offered or booked. false means unavailable/disabled and must not be shown as an available alternative."},"available_staff_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Appointment-only staff UUIDs available for this slot. 0 means not bookable; 1 may be autoselected; 2+ requires asking the user for a professional or \"sin preferencia\"."},"selected_staff_id":{"type":["string","null"],"format":"uuid","description":"Appointment-only backend default/suggestion. Do not use this to skip professional selection when available_staff_ids has more than one value."},"selected_staff_name":{"type":["string","null"],"description":"MCP-enriched appointment field when names can be resolved. If available_staff_count is 1, show this real professional in the summary and do not show \"Sin preferencia\"."},"available_staff":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"]}}},"description":"MCP-enriched appointment staff options for this slot. Use these names to show a concrete professional before confirmation."},"staff_selection_required":{"type":"boolean","description":"MCP-enriched appointment hint. true means multiple professionals are available; ask the user to choose or, after \"sin preferencia\", assign selected_staff_id and show selected_staff_name."},"staff_assignment_note":{"type":"string","description":"MCP-enriched actionable guidance for the agent. Field names in this note are internal and must not be shown to the customer."},"customer_facing_staff_phrase":{"type":["string","null"],"examples":["Como te da igual, te asigno a Paloma."],"description":"MCP-enriched safe wording for the customer. Prefer this kind of language over exposing internal field names."},"available_staff_count":{"type":"integer","minimum":0,"description":"Appointment-only count of available professionals for this slot."},"staff_capacity":{"type":"integer","minimum":0,"description":"Appointment-only count of staff considered for this service/slot."},"remaining_capacity":{"type":"integer","minimum":0},"slot_capacity":{"type":"integer","minimum":1},"party_size":{"type":"integer","minimum":1},"meal_period":{"$ref":"#/components/schemas/MealPeriod"},"turn_duration_minutes":{"type":"integer","minimum":1},"restaurant_zone_id":{"type":["string","null"],"format":"uuid","description":"Restaurant-only assigned/selected zone UUID for this slot when applicable."},"restaurant_zone_name":{"type":["string","null"],"description":"Restaurant-only zone label for this slot when applicable."},"restaurant_zone_slug":{"type":["string","null"],"description":"Restaurant-only zone slug for this slot when applicable."},"restaurant_zone_preference":{"anyOf":[{"$ref":"#/components/schemas/RestaurantZonePreference"},{"type":"null"}],"description":"Restaurant-only preference context for this slot when applicable."}}},"AvailabilityResponse":{"type":"object","required":["business","service","date","timezone","staff_id","slots"],"properties":{"business":{"$ref":"#/components/schemas/Business"},"service":{"$ref":"#/components/schemas/Service"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"timezone":{"type":"string","examples":["Europe/Madrid"]},"staff_id":{"type":["string","null"],"format":"uuid","description":"Selected staff member UUID, or null when availability is not staff-specific."},"service_id":{"type":["string","null"],"format":"uuid","description":"Selected service id, or null when not provided for restaurant availability."},"capacity_mode":{"$ref":"#/components/schemas/CapacityMode","description":"Business capacity mode. Restaurant-only."},"zone_selection_mode":{"anyOf":[{"$ref":"#/components/schemas/ZoneSelectionMode"},{"type":"null"}],"description":"Derived zone mode for restaurants: global, preference, or capacity."},"requested_zone":{"type":["object","null"],"properties":{"slug":{"type":["string","null"]},"name":{"type":["string","null"]},"preference":{"$ref":"#/components/schemas/RestaurantZonePreference"}},"description":"Restaurant requested zone context. In zones_preference this is not a capacity guarantee."},"requested_zone_available":{"type":["boolean","null"],"description":"When determinable, indicates whether the requested zone can satisfy the query context."},"available_zones":{"type":"array","items":{"$ref":"#/components/schemas/RestaurantZoneSummary"},"description":"Active restaurant zones for this business when applicable."},"zone_alternatives":{"type":"array","items":{"$ref":"#/components/schemas/RestaurantZoneSummary"},"description":"Alternative zones that may have availability when a requested zone is not available."},"active_zones":{"type":"array","items":{"$ref":"#/components/schemas/RestaurantActiveZone"},"description":"Restaurant zone list returned by backend availability when active zones exist."},"agent_instructions":{"type":["string","null"],"description":"Agent guidance for safe customer wording by capacity mode. In zones_preference, do not promise guaranteed zone capacity."},"available":{"type":"boolean","description":"Present for concrete restaurant availability checks."},"party_size":{"type":"integer","minimum":1},"meal_period":{"$ref":"#/components/schemas/MealPeriod"},"slots":{"type":"array","items":{"$ref":"#/components/schemas/Slot"}}}},"NextAvailabilityResponse":{"type":"object","required":["business_id","service_id","staff_id","timezone","next_slot"],"properties":{"business_id":{"type":"string","format":"uuid"},"service_id":{"type":"string","format":"uuid"},"staff_id":{"type":["string","null"],"format":"uuid","description":"Selected staff member UUID, or null when availability is not staff-specific."},"timezone":{"type":"string","examples":["Europe/Madrid"]},"next_slot":{"anyOf":[{"type":"object","required":["start_time","end_time"],"properties":{"start_time":{"type":"string","format":"date-time","examples":["2026-05-10T17:00:00+02:00"]},"end_time":{"type":"string","format":"date-time","examples":["2026-05-10T18:00:00+02:00"]}}},{"type":"null"}]}}},"CreateGlobalBookingRequest":{"type":"object","required":["business_id","service_id","start_time","customer"],"properties":{"business_id":{"type":"string","format":"uuid"},"service_id":{"type":"string","format":"uuid"},"staff_id":{"type":"string","format":"uuid","description":"Optional staff member UUID returned by listBusinessStaff. When provided, the booking is assigned to that professional."},"start_time":{"type":"string","format":"date-time","description":"Desired start datetime with timezone offset, for example 2026-05-10T17:00:00+02:00."},"customer":{"type":"object","required":["name","phone"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"email":{"type":"string","format":"email"},"phone":{"type":"string","minLength":1,"maxLength":30}}},"notes":{"type":"string","maxLength":1000},"agent_id":{"type":"string","minLength":1,"maxLength":100},"idempotency_key":{"type":"string","minLength":1,"maxLength":200,"description":"Accepted for forward compatibility. Persistence requires the idempotency_key migration."},"party_size":{"type":"integer","minimum":1,"description":"Required for restaurant bookings. Number of diners."},"meal_period":{"$ref":"#/components/schemas/MealPeriod","description":"Required for restaurant bookings."},"restaurant_zone_id":{"type":"string","format":"uuid","description":"Optional active restaurant zone UUID. Required when restaurant_zone_preference is zone."},"restaurant_zone_slug":{"type":"string","minLength":1,"maxLength":120,"description":"Optional active restaurant zone slug. APIs may resolve this to restaurant_zone_id before validation."},"restaurant_zone_preference":{"$ref":"#/components/schemas/RestaurantZonePreference","description":"Optional restaurant zone preference: zone for a concrete zone or no_preference when any zone is acceptable."},"turn_duration_minutes":{"type":"integer","minimum":1,"description":"Optional restaurant turn duration override."}}},"CreateGlobalBookingResponse":{"type":"object","required":["id","booking_reference","business_id","business_name","service_name","customer","start_time","end_time","status","price","currency","email_status"],"properties":{"id":{"type":"string","format":"uuid"},"booking_reference":{"type":"string","examples":["BK-2026-ABCD"]},"business_id":{"type":"string","format":"uuid"},"business_name":{"type":"string"},"service_name":{"type":"string"},"customer":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"email":{"type":["string","null"],"format":"email"},"phone":{"type":["string","null"]}}},"start_time":{"type":"string","format":"date-time"},"end_time":{"type":"string","format":"date-time"},"status":{"$ref":"#/components/schemas/BookingStatus"},"price":{"type":"integer","minimum":0},"currency":{"type":"string","examples":["EUR"]},"email_status":{"$ref":"#/components/schemas/EmailStatus"},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"}}},"BookingIntentRequest":{"type":"object","required":["intent"],"properties":{"intent":{"type":"string","minLength":1,"maxLength":500,"description":"Natural Spanish booking intent. Phase 1 supports category, city, date, time window, and simple service hints."},"customer":{"type":"object","required":["name","phone"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"email":{"type":"string","format":"email"},"phone":{"type":"string","minLength":1,"maxLength":30}}},"confirm":{"type":"boolean","default":false,"description":"When false, only return a proposal. When true, create the booking using selected_starts_at or the first suggested slot."},"selected_starts_at":{"type":"string","format":"date-time","description":"Exact slot selected by the user from a previous proposal."},"selected_business_slug":{"type":"string","minLength":1,"maxLength":200,"description":"Optional business slug selected from a previous response. When provided, the intent does not need to include category and city."},"selected_service_id":{"type":"string","format":"uuid","description":"Optional service UUID selected from a previous need_service response."}}},"BookingIntentProposalResponse":{"type":"object","required":["status","business","service","suggested_slots"],"properties":{"status":{"type":"string","const":"proposal"},"business":{"$ref":"#/components/schemas/Business"},"service":{"$ref":"#/components/schemas/Service"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"time_preference":{"type":"string","enum":["morning","afternoon","evening"]},"suggested_slots":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/Slot"}}}},"BookingIntentNeedServiceResponse":{"type":"object","required":["status","business","services","message"],"description":"Returned when a single business is found and it has active services, but the rule-based service hint does not match one confidently.","properties":{"status":{"type":"string","const":"need_service"},"business":{"$ref":"#/components/schemas/Business"},"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"message":{"type":"string","description":"Spanish message the agent can show to ask the user which service to book."}}},"BookingIntentConfirmedResponse":{"type":"object","required":["status","success","booking_id","business","service","starts_at","local_time"],"properties":{"status":{"type":"string","const":"confirmed"},"success":{"type":"boolean","const":true},"booking_id":{"type":"string","format":"uuid"},"booking_reference":{"type":"string","examples":["BK-2026-ABCD"]},"email_status":{"$ref":"#/components/schemas/EmailStatus"},"email_error":{"type":"string"},"business":{"$ref":"#/components/schemas/Business"},"service":{"$ref":"#/components/schemas/Service"},"starts_at":{"type":"string","format":"date-time"},"local_time":{"type":"string","examples":["16:30"]}}},"CreateBookingRequest":{"type":"object","required":["customer","confirmation_summary_presented","confirmation_obtained","user_confirmation_text"],"description":"Booking creation payload for a real side-effect action. Call only after showing a complete summary and receiving explicit user confirmation after that summary. If the user chose a concrete local_time such as 17:00, the summary must show exactly 17:00 and starts_at must come from the slot whose local_time is exactly 17:00. Confirming a booking for a different time is invalid unless the agent explicitly warned that the time changed and obtained a fresh post-summary confirmation for the new time. Never silently change \"A las 17:00 está bien\" into 19:00. Requires confirmation_summary_presented=true, confirmation_obtained=true, and user_confirmation_text from the post-summary confirmation. Providing missing name/phone/email is not confirmation. Choosing a time is not confirmation. Saying \"me da igual\", \"cualquiera\", or \"sin preferencia\" for professional is not confirmation. Saying \"reserva a nombre de X\" while giving initial data is not confirmation if the summary has not already been shown. If the user gives time + customer data + \"sin preferencia\" in one message, treat it as slot/staff/data collection only: locate the exact local_time slot, assign staff according to available_staff_count, show a summary with the same local_time and final staff name, then ask for confirmation. A second booking, even \"igual que la anterior\", still requires its own summary and explicit confirmation. Do not use an invented service_id; it must come from listBusinessServices. For exact appointment bookings, send service_id and starts_at from an available=true availability slot. If available_staff_count is 0, do not create. If available_staff_count is exactly 1, send the only available staff UUID in staff_id and show the real staff name, not \"Sin preferencia\". If available_staff_count is 2+ and the user chose \"sin preferencia\", prefer selecting a concrete available staff UUID before confirmation; selected_staff_id may be used only when it belongs to available_staff_ids. Technical field names can guide the agent but must not appear in customer-facing replies: do not say selected_staff_id, available_staff_ids, staff_id, local_time, MCP, schema, payload, UUID, or tool. Recommended wording: \"Como te da igual, te asigno a Paloma\" or \"Hay dos profesionales disponibles: Paloma y Sandra. Te asigno a Paloma.\" Claude/MCP rejects missing appointment staff_id with MCP_STAFF_ID_REQUIRED, staff names such as \"Sandra\" with MCP_STAFF_NAME_NOT_ALLOWED, \"sin preferencia\" with MCP_STAFF_ASSIGNMENT_REQUIRED, and malformed IDs with MCP_INVALID_STAFF_ID so agents can rectify instead of reporting a generic technical problem. Never send \"sin preferencia\", \"Sandra\", or another staff name as staff_id; staff_id must be a UUID/ID from checkBusinessAvailability or listBusinessStaff. If the final professional changes, the previous confirmation is invalid and a new summary plus fresh confirmation is required. After success, invalidate previous availability for the same business/service/date/time/staff. If creation fails because the slot or staff is unavailable, do not invent alternatives; check availability again first. For restaurants, send service_id, starts_at, party_size, and meal_period, and do not send staff_id. Restaurant zones are structured via restaurant_zone_id, restaurant_zone_slug, and restaurant_zone_preference (zone or no_preference). Do not send restaurant_zone_name_snapshot; snapshot is server-generated. In simple and zones_preference, zone is preference context and must not be presented as guaranteed capacity. In zones_capacity with active zones, create requires explicit zone intent: restaurant_zone_preference=no_preference or preference=zone with a valid active zone id/slug. If the requested zone is unavailable, check availability again and offer only real alternatives. For natural appointment inputs, send service_name plus date and time_preference.","allOf":[{"anyOf":[{"required":["service_id"]},{"required":["service_name"]}]},{"anyOf":[{"required":["starts_at"]},{"required":["date","time_preference"]}]}],"properties":{"service_id":{"type":"string","format":"uuid","description":"Exact active service id. Preferred when already known."},"staff_id":{"type":"string","format":"uuid","description":"Appointment staff member UUID returned by checkBusinessAvailability/listBusinessStaff. Must be a real UUID/ID, never a staff name such as \"Sandra\" and never text such as \"sin preferencia\". Required by Claude/MCP for appointment booking creation after resolving the selected slot. For 2+ professionals with \"sin preferencia\", assign and send a concrete available staff UUID before confirmation. Do not send for restaurants."},"service_name":{"type":"string","description":"Natural service name to match against active services, case-insensitive and partial.","examples":["Mechas"]},"starts_at":{"type":"string","format":"date-time","description":"Exact slot datetime copied from availability starts_at. If the user chose a concrete local_time, this must be the starts_at for that exact local_time."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Desired local business date when starts_at is not known.","examples":["2026-05-07"]},"time_preference":{"type":"string","enum":["morning","afternoon","evening"],"description":"Preferred local time window. morning=08:00-12:00, afternoon=12:00-17:00, evening=17:00-21:00."},"party_size":{"type":"integer","minimum":1,"description":"Required for restaurant bookings. Number of diners."},"meal_period":{"$ref":"#/components/schemas/MealPeriod","description":"Required for restaurant bookings."},"restaurant_zone_id":{"type":"string","format":"uuid","description":"Optional active restaurant zone UUID. Required when restaurant_zone_preference is zone."},"restaurant_zone_slug":{"type":"string","minLength":1,"maxLength":120,"description":"Optional active restaurant zone slug. APIs may resolve this to restaurant_zone_id before validation."},"restaurant_zone_preference":{"$ref":"#/components/schemas/RestaurantZonePreference","description":"Optional restaurant zone preference: zone for a concrete zone or no_preference when any zone is acceptable."},"turn_duration_minutes":{"type":"integer","minimum":1,"description":"Optional restaurant turn duration override."},"confirmation_summary_presented":{"type":"boolean","const":true,"description":"Required agent guard. Must be true only after the complete booking summary was shown to the user."},"confirmation_obtained":{"type":"boolean","const":true,"description":"Required agent guard. Must be true only after the user explicitly confirmed after the complete summary."},"user_confirmation_text":{"type":"string","minLength":1,"maxLength":300,"description":"Exact post-summary confirmation text, such as \"sí, confirmo\", \"confirmo\", \"adelante\", or \"haz la reserva\". Choosing a time, providing contact data, saying \"sin preferencia\", or saying \"reserva a nombre de X\" while giving initial data is not valid confirmation."},"customer":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"email":{"type":"string","format":"email"},"phone":{"type":"string","maxLength":30}}}}},"CreateBookingResponse":{"type":"object","required":["success","booking_id","starts_at","local_time"],"properties":{"success":{"type":"boolean","const":true},"booking_id":{"type":"string","format":"uuid"},"booking_reference":{"type":"string","examples":["BK-2026-ABCD"]},"email_status":{"$ref":"#/components/schemas/EmailStatus"},"email_error":{"type":"string"},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"},"starts_at":{"type":"string","format":"date-time"},"local_time":{"type":"string","examples":["16:00"]}}},"SearchCustomerBookingsResponse":{"type":"object","properties":{"count":{"type":"integer","description":"Number of phone-matched bookings when details are not returned."},"requires_verification":{"type":"boolean","description":"True when OTP verification is required before details can be returned."},"verification_method":{"type":"string","const":"email_otp"},"next_action":{"type":"string","const":"request_customer_booking_verification_code"},"message":{"type":"string","examples":["Para ver los detalles, solicita un código de verificación enviado al email asociado."]},"bookings":{"type":"array","items":{"$ref":"#/components/schemas/SearchCustomerBookingItem"}},"active_bookings":{"type":"array","description":"Active bookings (confirmed/rescheduled) prioritized for agent responses.","items":{"$ref":"#/components/schemas/SearchCustomerBookingItem"}},"history_bookings":{"type":"array","description":"Historical bookings shown after active bookings.","items":{"$ref":"#/components/schemas/SearchCustomerBookingItem"}},"cancelled_bookings":{"type":"array","description":"Cancelled/completed/no_show bookings for history context.","items":{"$ref":"#/components/schemas/SearchCustomerBookingItem"}},"has_active_bookings":{"type":"boolean","description":"True when at least one confirmed/rescheduled booking exists for the verified customer."},"has_more_results":{"type":"boolean","description":"True when there are more matching bookings than the returned limit."},"limit":{"type":"integer","minimum":1,"maximum":50},"total_matches":{"type":"integer","minimum":0},"active_total":{"type":"integer","minimum":0},"history_total":{"type":"integer","minimum":0},"cancelled_total":{"type":"integer","minimum":0}}},"SearchCustomerBookingItem":{"type":"object","required":["id","booking_reference","business_name","service_name","staff_name","starts_at","ends_at","status"],"properties":{"id":{"type":"string","format":"uuid"},"booking_reference":{"type":"string","examples":["BK-2026-ABCD"]},"business_name":{"type":"string","examples":["Peluqueria Loli"]},"business_ref":{"type":["string","null"],"examples":["peluqueria-loli"]},"business_type":{"type":["string","null"],"enum":["appointment","restaurant","generic",null]},"customer_name":{"type":["string","null"]},"service_name":{"type":["string","null"],"examples":["Mechas"]},"staff_name":{"type":["string","null"],"examples":["Laura"]},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"},"created_at":{"type":["string","null"],"format":"date-time"},"updated_at":{"type":["string","null"],"format":"date-time"},"status":{"$ref":"#/components/schemas/BookingStatus"},"party_size":{"type":["integer","null"],"minimum":1},"meal_period":{"type":["string","null"],"enum":["lunch","dinner",null]},"restaurant_zone_id":{"type":["string","null"],"format":"uuid"},"restaurant_zone_name_snapshot":{"type":["string","null"]},"restaurant_zone_slug":{"type":["string","null"]},"restaurant_zone_preference":{"type":["string","null"],"enum":["zone","no_preference",null]},"notes":{"type":["string","null"]}}},"RequestCustomerBookingVerificationCodeRequest":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","minLength":1,"maxLength":30}}},"RequestCustomerBookingVerificationCodeResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["code_sent","email_required"]},"masked_email":{"type":"string","examples":["a***@example.com"]},"message":{"type":"string","examples":["Si encontramos reservas asociadas, enviaremos un código al email registrado."]}}},"RescheduleBookingRequest":{"type":"object","required":["starts_at","customer_verification","confirmation_summary_presented","confirmation_obtained","user_confirmation_text"],"description":"Business-scoped reschedule payload used by MCP/business API clients. The service is kept from the existing booking. Requires a valid OTP verification_code plus explicit confirmation after showing the old/new summary. Booking_ref, phone, email, or a textual \"si, cambiala\" alone are not sufficient. For appointment bookings, send staff_id if the user chooses a different professional; if omitted, BKlayer keeps the original staff member. BKlayer must not silently assign another professional when staff_id is provided. For restaurant bookings, send party_size when it changes and meal_period when changing lunch/comida vs dinner/cena or when the target slot depends on meal period. If the user changes the number of people, party_size is mandatory in this reschedule request. Do not rely on a prior availability query party_size; availability checks do not update the booking. Never send staff_id for restaurants. Restaurant zone fields are optional and additive: restaurant_zone_id, restaurant_zone_slug, and restaurant_zone_preference (zone or no_preference). If the user requests a zone change, include restaurant_zone_slug or restaurant_zone_id plus restaurant_zone_preference=zone in this same request. If restaurant_zone_* is omitted, BKlayer keeps the current zone assignment. Do not send restaurant_zone_name_snapshot; it is server-generated. The public /api/v1/bookings/{id}/reschedule route does not accept restaurant_zone_* inputs and preserves the existing restaurant zone.","properties":{"starts_at":{"type":"string","format":"date-time","description":"New exact slot datetime from checkBusinessAvailability."},"staff_id":{"type":"string","format":"uuid","description":"Appointment only. Required when the user explicitly changes professional. If omitted for appointments, BKlayer keeps the original staff member. Never send for restaurants."},"party_size":{"type":"integer","minimum":1,"description":"Restaurant only. New party size. If the user changes the number of guests, send this field in the reschedule request even if availability was checked with the same party_size. If omitted, BKlayer keeps the existing party_size."},"partySize":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"new_party_size":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"guests":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"people":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"meal_period":{"type":"string","enum":["lunch","dinner"],"description":"Restaurant only. Required when changing lunch/comida vs dinner/cena, or when the target slot depends on meal period."},"restaurant_zone_id":{"type":"string","format":"uuid","description":"Optional active restaurant zone UUID for reschedule. Use with restaurant_zone_preference=zone."},"restaurant_zone_slug":{"type":"string","minLength":1,"maxLength":120,"description":"Optional active restaurant zone slug. APIs may resolve this to restaurant_zone_id before validation."},"restaurant_zone_preference":{"type":"string","enum":["zone","no_preference"],"description":"Optional restaurant zone preference: zone for a concrete zone or no_preference when any zone is acceptable."},"turn_duration_minutes":{"type":"integer","minimum":1,"description":"Restaurant only. Optional turn duration from the selected available slot."},"customer_name":{"type":"string","minLength":1,"maxLength":200,"description":"Optional updated customer name when the user explicitly changes it."},"customer_verification":{"type":"object","description":"Customer OTP verification required before public agents can reschedule. Get the code with requestCustomerBookingVerificationCode using the same business and phone.","required":["phone","verification_code"],"properties":{"phone":{"type":"string","maxLength":30},"verification_code":{"type":"string","pattern":"^\\d{6}$","description":"Six-digit OTP sent to the booking customer email."}}},"confirmation_summary_presented":{"type":"boolean","const":true,"description":"Must be true only after the agent showed a complete old/new reschedule summary."},"confirmation_obtained":{"type":"boolean","const":true,"description":"Must be true only after the user explicitly confirmed that exact summary."},"user_confirmation_text":{"type":"string","minLength":1,"maxLength":300,"description":"Exact post-summary confirmation text from the user. Choosing a new time or providing contact data is not enough."}}},"RescheduleBookingResponse":{"type":"object","required":["success","booking_id","starts_at","local_time","status"],"properties":{"success":{"type":"boolean","const":true},"booking_id":{"type":"string","format":"uuid"},"booking_ref":{"type":"string","examples":["BK-2026-7SND"]},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"},"local_time":{"type":"string","examples":["18:00"]},"status":{"type":"string","const":"rescheduled"},"staff_id":{"type":["string","null"],"format":"uuid","description":"Present for appointment reschedules."},"staff_name":{"type":["string","null"],"description":"Present for appointment reschedules when staff can be resolved."},"party_size":{"type":"integer","minimum":1,"description":"Present for restaurant reschedules."},"meal_period":{"type":"string","enum":["lunch","dinner"],"description":"Present for restaurant reschedules."},"turn_duration_minutes":{"type":"integer","minimum":1,"description":"Present for restaurant reschedules."},"restaurant_zone_id":{"type":["string","null"],"format":"uuid","description":"Present for restaurant reschedules when a zone is assigned."},"restaurant_zone_name_snapshot":{"type":["string","null"],"description":"Present for restaurant reschedules when a zone snapshot is available."},"restaurant_zone_preference":{"type":["string","null"],"enum":["zone","no_preference",null],"description":"Present for restaurant reschedules when zone preference exists."},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"}}},"CancelBookingRequest":{"type":"object","required":["customer_verification","confirmation_summary_presented","confirmation_obtained","user_confirmation_text"],"description":"Public agent cancellation payload. Requires a valid OTP verification_code plus explicit confirmation after showing the cancellation summary. Booking_ref, phone, email, or \"si, cancelala\" alone are not sufficient.","properties":{"customer_verification":{"type":"object","description":"Customer OTP verification required before public agents can cancel. Get the code with requestCustomerBookingVerificationCode using the same business and phone.","required":["phone","verification_code"],"properties":{"phone":{"type":"string","minLength":1,"maxLength":30},"verification_code":{"type":"string","pattern":"^\\d{6}$","description":"Six-digit OTP sent to the booking customer email."}}},"confirmation_summary_presented":{"type":"boolean","const":true,"description":"Must be true only after the agent showed the exact booking cancellation summary."},"confirmation_obtained":{"type":"boolean","const":true,"description":"Must be true only after the user explicitly confirmed that exact cancellation summary."},"user_confirmation_text":{"type":"string","minLength":1,"maxLength":300,"description":"Exact post-summary confirmation text from the user. Phone, email, booking_ref, or initial cancellation intent are not enough."}}},"BookingResponse":{"type":"object","description":"Raw booking record shape returned by dashboard-oriented endpoints.","properties":{"id":{"type":"string","format":"uuid"},"booking_ref":{"type":"string","examples":["BK-2026-ABCD"]},"business_id":{"type":"string","format":"uuid"},"service_id":{"type":"string","format":"uuid"},"staff_id":{"type":["string","null"],"format":"uuid"},"customer_id":{"type":["string","null"],"format":"uuid"},"customer_name":{"type":["string","null"]},"customer_email":{"type":["string","null"],"format":"email"},"customer_phone":{"type":["string","null"]},"agent_id":{"type":["string","null"]},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"},"status":{"$ref":"#/components/schemas/BookingStatus"},"notes":{"type":["string","null"]},"google_event_id":{"type":["string","null"]},"idempotency_key":{"type":["string","null"]}}},"BookingUpdateRequest":{"type":"object","required":["customer_name"],"properties":{"customer_name":{"type":"string","minLength":1,"maxLength":200},"customer_email":{"anyOf":[{"type":"string","format":"email","maxLength":254},{"type":"string","const":""}]},"customer_phone":{"anyOf":[{"type":"string","maxLength":30},{"type":"string","const":""}]},"notes":{"anyOf":[{"type":"string","maxLength":1000},{"type":"string","const":""}]}}},"BookingUpdateResponse":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","required":["id","customer_name","customer_email","customer_phone","notes"],"properties":{"id":{"type":"string","format":"uuid"},"customer_name":{"type":["string","null"]},"customer_email":{"type":["string","null"],"format":"email"},"customer_phone":{"type":["string","null"]},"notes":{"type":["string","null"]}}},"event":{"type":["object","null"],"additionalProperties":true},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"}}},"PublicRescheduleBookingRequest":{"type":"object","required":["starts_at"],"description":"Public /api/v1/bookings/{id}/reschedule payload. For customer-agent flows, send customer_verification plus explicit confirmation fields. Dashboard owner sessions can omit customer_verification. Restaurant zone changes are not supported on this public route: do not send restaurant_zone_id, restaurant_zone_slug, or restaurant_zone_preference. BKlayer validates restaurant capacity against the existing booking zone and preserves that zone.","properties":{"starts_at":{"type":"string","format":"date-time"},"staff_id":{"type":"string","format":"uuid","description":"Appointment only. New staff member when explicitly selected. If omitted, the original staff member is kept."},"party_size":{"type":"integer","minimum":1,"description":"Restaurant only. New party size. If omitted, the existing party size is kept."},"partySize":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"new_party_size":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"guests":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"people":{"type":"integer","minimum":1,"description":"Compatibility alias for party_size. Prefer party_size."},"meal_period":{"type":"string","enum":["lunch","dinner"],"description":"Restaurant only. Target meal period."},"mealPeriod":{"type":"string","enum":["lunch","dinner"],"description":"Compatibility alias for meal_period. Prefer meal_period."},"new_meal_period":{"type":"string","enum":["lunch","dinner"],"description":"Compatibility alias for meal_period. Prefer meal_period."},"turn_duration_minutes":{"type":"integer","minimum":1,"description":"Restaurant only. Target turn duration."},"turnDurationMinutes":{"type":"integer","minimum":1,"description":"Compatibility alias for turn_duration_minutes. Prefer turn_duration_minutes."},"new_turn_duration_minutes":{"type":"integer","minimum":1,"description":"Compatibility alias for turn_duration_minutes. Prefer turn_duration_minutes."},"customer_name":{"type":"string","minLength":1,"maxLength":200},"agent_id":{"type":"string","minLength":1,"maxLength":100},"customer_verification":{"type":"object","description":"Customer OTP verification for public/agent reschedule. Required when no authenticated dashboard owner session is used.","required":["phone","verification_code"],"properties":{"phone":{"type":"string","maxLength":30},"verification_code":{"type":"string","pattern":"^\\d{6}$"}}},"confirmation_summary_presented":{"type":"boolean","const":true,"description":"Required for public/agent reschedule. Must be true only after the old/new summary was shown."},"confirmation_obtained":{"type":"boolean","const":true,"description":"Required for public/agent reschedule. Must be true only after explicit confirmation."},"user_confirmation_text":{"type":"string","minLength":1,"maxLength":300,"description":"Exact post-summary confirmation text from the user."}}},"DashboardRescheduleBookingResponse":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BookingResponse"},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"}}},"BookingCancelResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","const":true},"booking_reference":{"type":"string","examples":["BK-2026-ABCD"]},"status":{"type":"string","const":"cancelled"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"booking_ref":{"type":"string","examples":["BK-2026-ABCD"]},"status":{"type":"string","const":"cancelled"}}},"calendar_status":{"$ref":"#/components/schemas/CalendarStatus"},"calendar_error":{"$ref":"#/components/schemas/CalendarError"}}},"BookingCompleteResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","const":true},"status":{"type":"string","const":"completed"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","const":"completed"}}}}},"ApiError":{"type":"object","required":["error","code"],"properties":{"error":{"type":"string"},"code":{"type":"string"}}},"NoAvailabilityResponse":{"type":"object","required":["error","code","alternatives"],"description":"Availability error with up to 3 suggested slots sorted by proximity to the requested time or preferred time window.","properties":{"error":{"type":"string"},"code":{"type":"string","const":"NO_AVAILABILITY"},"alternatives":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/Slot"}}}},"SlotUnavailableResponse":{"type":"object","required":["error","code","alternatives"],"properties":{"error":{"type":"string"},"code":{"type":"string","const":"SLOT_UNAVAILABLE"},"alternatives":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/Slot"}}}}}}}