{"openapi":"3.1.0","info":{"title":"Gestión Agenda public booking API","version":"1.0.0","description":"Use this API to find salon services and available times, then book for a customer. The external agent must get customer approval in its own conversation before it sends the booking request. This API does not perform a separate approval or one-time code step. No admin login is required. A displayed slot is not held: submit the booking and handle a 409 if availability changed. Times use ISO 8601 offsets; date search uses the salon's America/Montevideo calendar. Prices are in UYU."},"servers":[{"url":"/api/v1"}],"tags":[{"name":"Public booking"},{"name":"Customer cancellation"}],"components":{"schemas":{"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}},"BookingOptions":{"type":"object","required":["salon","locations","categories","services","compatibilityByLocation","professionalsByService","dates","slots"],"properties":{"salon":{"type":"object","required":["name","tagline"],"properties":{"name":{"type":"string"},"tagline":{"type":"string"}}},"locations":{"type":"array","items":{"type":"object","required":["id","name","address","hours"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"},"googleMapsUrl":{"type":["string","null"]},"hours":{"type":"string"}}}},"categories":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"}},"additionalProperties":true}},"services":{"type":"array","items":{"type":"object","required":["id","name","categoryId","durationMinutes","currency","regularPrice","effectivePrice","isOnSale"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"categoryId":{"type":"string"},"durationMinutes":{"type":"integer"},"currency":{"type":"string","examples":["UYU"]},"regularPrice":{"type":"integer"},"effectivePrice":{"type":"integer","description":"Current bookable price in the service currency."},"isOnSale":{"type":"boolean"}},"additionalProperties":true}},"compatibilityByLocation":{"type":"object","additionalProperties":true,"description":"Service and location compatibility keyed by location ID."},"professionalsByService":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","required":["id","name","initials","role"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"initials":{"type":"string"},"role":{"type":"string"}}}}},"dates":{"type":"array","items":{"type":"object","required":["value","weekday","day","month"],"properties":{"value":{"type":"string","format":"date"},"weekday":{"type":"string"},"day":{"type":"string"},"month":{"type":"string"}}}},"slots":{"type":"array","items":{"type":"object","required":["startsAt","endsAt","label","segments"],"properties":{"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"label":{"type":"string"},"segments":{"type":"array","items":{"type":"object","required":["serviceId","serviceName","serviceCategory","professionalId","professionalName","assignmentPending","startsAt","endsAt","durationMinutes","price"],"properties":{"serviceId":{"type":"string"},"serviceName":{"type":"string"},"serviceCategory":{"type":"string"},"professionalId":{"type":"string"},"professionalName":{"type":"string"},"assignmentPending":{"type":"boolean"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"durationMinutes":{"type":"integer"},"price":{"type":"integer","description":"Price in UYU."}}}}}}}}},"BookingRequest":{"type":"object","required":["locationId","items","schedule","customerName","email","phone","consent"],"properties":{"locationId":{"type":"string","minLength":1},"items":{"type":"array","minItems":1,"maxItems":6,"description":"Use each service only once. Use professionalId 'any' for any eligible professional.","items":{"type":"object","required":["serviceId"],"properties":{"serviceId":{"type":"string"},"professionalId":{"type":"string","default":"any"}}}},"schedule":{"oneOf":[{"type":"object","required":["mode","startsAt"],"properties":{"mode":{"const":"consecutive"},"startsAt":{"type":"string","format":"date-time","description":"Start time from a returned consecutive slot."}}},{"type":"object","required":["mode","appointments"],"properties":{"mode":{"const":"separate"},"appointments":{"type":"array","minItems":1,"maxItems":6,"description":"Exactly one selected slot per service.","items":{"type":"object","required":["serviceId","startsAt"],"properties":{"serviceId":{"type":"string"},"startsAt":{"type":"string","format":"date-time"}}}}}}]},"customerName":{"type":"string","minLength":2,"maxLength":120},"email":{"type":"string","format":"email","maxLength":160},"phone":{"type":"string","minLength":8,"maxLength":30},"notes":{"type":"string","maxLength":600},"consent":{"const":true,"description":"Set true only after the customer approves this booking and use of their contact details in the agent conversation. The API accepts this assertion without a separate verification step."},"website":{"type":"string","maxLength":0,"description":"Spam trap. Omit this field."}}},"BookingConfirmation":{"type":"object","required":["ok","bookingId","confirmationCode","locationName","appointments","totalprice"],"properties":{"ok":{"const":true},"bookingId":{"type":"string"},"confirmationCode":{"type":"string"},"locationName":{"type":"string"},"appointments":{"type":"array","items":{"allOf":[{"type":"object","required":["serviceId","serviceName","serviceCategory","professionalId","professionalName","assignmentPending","startsAt","endsAt","durationMinutes","price"],"properties":{"serviceId":{"type":"string"},"serviceName":{"type":"string"},"serviceCategory":{"type":"string"},"professionalId":{"type":"string"},"professionalName":{"type":"string"},"assignmentPending":{"type":"boolean"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"durationMinutes":{"type":"integer"},"price":{"type":"integer","description":"Price in UYU."}}},{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}]}},"totalprice":{"type":"integer","description":"Total in UYU. The server sets the final price."}}},"CustomerReservation":{"type":"object","required":["serviceName","startsAt","endsAt","status","canCancel","location","professionalName"],"properties":{"serviceName":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["RESERVED","CONFIRMED","CHECKED_IN","COMPLETED","NO_SHOW","CANCELLED"]},"canCancel":{"type":"boolean"},"professionalName":{"type":"string"},"location":{"type":"object","required":["name","address","googleMapsUrl","phone","timeZone"],"properties":{"name":{"type":"string"},"address":{"type":"string"},"googleMapsUrl":{"type":["string","null"]},"phone":{"type":["string","null"]},"timeZone":{"type":"string"}}}}},"CancellationResult":{"type":"object","required":["reservation","alreadyCancelled"],"properties":{"reservation":{"$ref":"#/components/schemas/CustomerReservation"},"alreadyCancelled":{"type":"boolean"}}}}},"paths":{"/public/booking-options":{"get":{"operationId":"findBookingOptions","tags":["Public booking"],"summary":"Find services, professionals, dates, and free times","description":"Call first without filters to discover locations and services. Then send locationId, one or more serviceId values, mode, and date to get slots. For separate mode, set activeServiceId to the service being scheduled. professionalIds is a JSON object keyed by service ID. Availability can change before booking.","parameters":[{"name":"locationId","in":"query","schema":{"type":"string"}},{"name":"serviceId","in":"query","description":"Repeat for each selected service, up to six.","schema":{"type":"array","maxItems":6,"items":{"type":"string"}},"style":"form","explode":true},{"name":"mode","in":"query","schema":{"type":"string","enum":["consecutive","separate"]}},{"name":"professionalIds","in":"query","description":"JSON object such as {\"service-1\":\"any\"}.","schema":{"type":"string"}},{"name":"activeServiceId","in":"query","description":"Required for slots in separate mode.","schema":{"type":"string"}},{"name":"date","in":"query","description":"Calendar date in America/Montevideo.","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Catalog and matching slots","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BookingOptions"}}}}}},"400":{"description":"Invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid filter values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/public/bookings":{"post":{"operationId":"createPublicBooking","tags":["Public booking"],"summary":"Create a customer booking","description":"In the agent conversation, ask the customer to approve the selected location, services, times, total price, and contact details before this call. The API does not request an email or SMS code. Generate one random UUID for each intended booking. Reuse the same Idempotency-Key and identical body for a retry. A repeated successful request returns the original 201 response. A key used with different data returns 422. The response does not include a cancellation token; cancellation links are sent through the salon's customer notification flow.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"description":"Random UUID for this intended booking. Keep it unchanged for retries. Never reuse it for another booking.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRequest"}}}},"responses":{"201":{"description":"Booking created or safely replayed","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BookingConfirmation"}}}}}},"400":{"description":"Invalid JSON or missing Idempotency-Key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Location or service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Slot conflict or booking still in progress; retry the same key when appropriate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invalid fields or key reused with other data","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many booking attempts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/public/appointments/{token}":{"parameters":[{"name":"token","in":"path","required":true,"description":"Secret cancellation token sent to the customer. Never log or share it.","schema":{"type":"string","format":"uuid"}}],"get":{"operationId":"getCustomerReservation","tags":["Customer cancellation"],"summary":"Read a booking with its customer token","responses":{"200":{"description":"Reservation","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CustomerReservation"}}}}}},"404":{"description":"Unknown or unavailable token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"operationId":"cancelCustomerReservation","tags":["Customer cancellation"],"summary":"Cancel a booking with its customer token","description":"Only use the token if the customer gave it to you and asked to cancel. Repeated cancellation returns alreadyCancelled=true.","responses":{"200":{"description":"Cancellation result","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CancellationResult"}}}}}},"404":{"description":"Unknown or unavailable token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Booking can no longer be cancelled online","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Too many requests","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}