{"openapi":"3.1.0","info":{"title":"Chinese Massage Oasis booking API","version":"1.0.0","summary":"Browse services and open times, and hold, confirm, cancel or reschedule appointments.","description":"Public REST API for Chinese Massage Oasis in Murray, Utah. No payment is taken through this API: the customer pays in person at the spa (a 4.95% surcharge applies to card and Venmo payments). POST /bookings creates a pending hold for 10 minutes and emails a 6-digit code to the customer; POST /bookings/{ref}/confirm with that code books the appointment. Times are in America/Denver. Errors always look like {\"error\":{\"code\",\"message\"}}. Writes are rate limited per IP and per phone number (HTTP 429, Retry-After)."},"servers":[{"url":"https://jade-oasis.vercel.app/api/v1"}],"tags":[{"name":"Catalog","description":"Read-only services and availability."},{"name":"Bookings","description":"Hold, confirm, inspect, cancel and reschedule appointments."}],"paths":{"/services":{"get":{"operationId":"listServices","tags":["Catalog"],"summary":"List services with duration and price","responses":{"200":{"description":"Active services.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicesResponse"}}}},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}},"/availability":{"get":{"operationId":"getAvailability","tags":["Catalog"],"summary":"Open start times for a service on a date or a date range","description":"Provide date, or from (and optionally to). Ranges are limited to 14 days. Pending holds count as taken.","parameters":[{"name":"service_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"date","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Open times.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"404":{"$ref":"#/components/responses/Error404"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}},"/bookings":{"post":{"operationId":"createBooking","tags":["Bookings"],"summary":"Hold a slot (status pending for 10 minutes)","description":"Takes no payment. Emails a 6-digit code to the customer; confirm it with POST /bookings/{ref}/confirm. Keep manage_token: it is shown once and only a hash is stored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookingRequest"}}}},"responses":{"201":{"description":"Pending booking created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBookingResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"},"503":{"$ref":"#/components/responses/Error503"}}}},"/bookings/{ref}":{"get":{"operationId":"getBooking","tags":["Bookings"],"summary":"Read a booking","parameters":[{"name":"ref","in":"path","required":true,"description":"Booking reference, e.g. CMO-1A2B3C4D.","schema":{"type":"string","pattern":"^CMO-[0-9A-Za-z]{8}$"}}],"security":[{"manageToken":[]}],"responses":{"200":{"description":"The booking.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}},"/bookings/{ref}/confirm":{"post":{"operationId":"confirmBooking","tags":["Bookings"],"summary":"Confirm a pending booking with the emailed code","description":"Five wrong codes lock the booking. Expired holds return EXPIRED.","parameters":[{"name":"ref","in":"path","required":true,"description":"Booking reference, e.g. CMO-1A2B3C4D.","schema":{"type":"string","pattern":"^CMO-[0-9A-Za-z]{8}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmBookingRequest"}}}},"responses":{"200":{"description":"Booking confirmed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"410":{"$ref":"#/components/responses/Error410"},"422":{"$ref":"#/components/responses/Error422"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}},"/bookings/{ref}/cancel":{"post":{"operationId":"cancelBooking","tags":["Bookings"],"summary":"Cancel a pending or booked appointment","parameters":[{"name":"ref","in":"path","required":true,"description":"Booking reference, e.g. CMO-1A2B3C4D.","schema":{"type":"string","pattern":"^CMO-[0-9A-Za-z]{8}$"}}],"security":[{"manageToken":[]},{}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManageTokenBody"}}}},"responses":{"200":{"description":"Cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}},"/bookings/{ref}/reschedule":{"post":{"operationId":"rescheduleBooking","tags":["Bookings"],"summary":"Move a booking to a new time (same service)","parameters":[{"name":"ref","in":"path","required":true,"description":"Booking reference, e.g. CMO-1A2B3C4D.","schema":{"type":"string","pattern":"^CMO-[0-9A-Za-z]{8}$"}}],"security":[{"manageToken":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RescheduleBookingRequest"}}}},"responses":{"200":{"description":"Rescheduled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"400":{"$ref":"#/components/responses/Error400"},"401":{"$ref":"#/components/responses/Error401"},"404":{"$ref":"#/components/responses/Error404"},"409":{"$ref":"#/components/responses/Error409"},"429":{"$ref":"#/components/responses/Error429"},"500":{"$ref":"#/components/responses/Error500"}}}}},"components":{"securitySchemes":{"manageToken":{"type":"http","scheme":"bearer","description":"The manage_token returned by POST /bookings. It may instead be sent as manage_token in the JSON body."}},"responses":{"Error400":{"description":"VALIDATION: the request was malformed or a field was invalid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error401":{"description":"UNAUTHORIZED: missing or wrong manage_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error404":{"description":"NOT_FOUND.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error409":{"description":"SLOT_TAKEN: that time is no longer open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error410":{"description":"EXPIRED: the 10 minute hold ran out.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error422":{"description":"BOOKING_CAP or INVALID_CODE.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error429":{"description":"RATE_LIMITED. See the Retry-After header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error500":{"description":"UNEXPECTED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Error503":{"description":"EMAIL_NOT_CONFIGURED: the confirmation email cannot be sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["VALIDATION","SLOT_TAKEN","RATE_LIMITED","BOOKING_CAP","NOT_FOUND","UNAUTHORIZED","EXPIRED","INVALID_CODE","EMAIL_NOT_CONFIGURED","UNEXPECTED"]},"message":{"type":"string"}}}}},"Service":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"duration_minutes":{"type":"integer"},"price_usd":{"type":"number"}}},"ServicesResponse":{"type":"object","properties":{"services":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"payment":{"type":"string"}}},"AvailabilityResponse":{"type":"object","properties":{"timezone":{"type":"string","examples":["America/Denver"]},"service":{"$ref":"#/components/schemas/Service"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"open_times":{"type":"array","items":{"type":"string","examples":["14:00"]}}}}},"note":{"type":"string"}}},"Booking":{"type":"object","properties":{"reference":{"type":"string","examples":["CMO-1A2B3C4D"]},"status":{"type":"string","enum":["pending","booked","cancelled","expired","completed","no_show"]},"source":{"type":"string","enum":["web","admin","api","mcp"]},"service":{"$ref":"#/components/schemas/Service"},"date":{"type":"string","format":"date"},"start_time":{"type":"string"},"end_time":{"type":"string"},"start":{"type":"string","format":"date-time"},"timezone":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","description":"Masked."}}},"hold_expires_at":{"type":["string","null"],"format":"date-time"},"payment":{"type":"string"}}},"BookingResponse":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"},"confirmed":{"type":"boolean"},"cancelled":{"type":"boolean"},"rescheduled":{"type":"boolean"},"already_confirmed":{"type":"boolean"},"already_cancelled":{"type":"boolean"}}},"CreateBookingResponse":{"type":"object","properties":{"booking":{"$ref":"#/components/schemas/Booking"},"manage_token":{"type":"string","description":"Shown once. Required to cancel, reschedule or read the booking."},"confirmation":{"type":"object","properties":{"method":{"type":"string"},"expires_at":{"type":"string","format":"date-time"},"next":{"type":"string"}}}}},"CreateBookingRequest":{"type":"object","properties":{"service_id":{"type":"string","description":"Service id from list_services / GET /services."},"start":{"description":"Appointment start. ISO 8601 with an offset (2026-10-14T14:00:00-06:00) or local America/Denver time without an offset (2026-10-14T14:00). Alternative to date + time.","type":"string","minLength":10,"maxLength":40},"date":{"description":"Local date YYYY-MM-DD (America/Denver). Use with time instead of start.","type":"string"},"time":{"description":"Local start time HH:MM, 24h (America/Denver). Use with date instead of start.","type":"string"},"name":{"type":"string","minLength":2,"maxLength":100,"description":"Customer full name."},"phone":{"type":"string","maxLength":30,"description":"Customer phone number (at least 10 digits)."},"email":{"description":"Customer email. The confirmation code is sent here.","type":"string","maxLength":160}},"required":["service_id","name","phone","email"]},"ConfirmBookingRequest":{"type":"object","properties":{"code":{"description":"6-digit code from the confirmation email.","type":"string","pattern":"^\\d{6}$"},"token":{"description":"Link token from the confirmation email link.","type":"string","minLength":8,"maxLength":200}}},"RescheduleBookingRequest":{"type":"object","properties":{"start":{"description":"Appointment start. ISO 8601 with an offset (2026-10-14T14:00:00-06:00) or local America/Denver time without an offset (2026-10-14T14:00). Alternative to date + time.","type":"string","minLength":10,"maxLength":40},"date":{"description":"Local date YYYY-MM-DD (America/Denver). Use with time instead of start.","type":"string"},"time":{"description":"Local start time HH:MM, 24h (America/Denver). Use with date instead of start.","type":"string"},"manage_token":{"description":"Token returned when the booking was created. Alternatively send Authorization: Bearer <token>.","type":"string","minLength":8,"maxLength":200}}},"ManageTokenBody":{"type":"object","properties":{"manage_token":{"description":"Token returned when the booking was created. Alternatively send Authorization: Bearer <token>.","type":"string","minLength":8,"maxLength":200}}},"AvailabilityQuery":{"type":"object","properties":{"service_id":{"type":"string","description":"Service to book (determines session length)."},"date":{"description":"A single day, YYYY-MM-DD.","type":"string"},"from":{"description":"First day of a range, YYYY-MM-DD. Ranges are at most 14 days.","type":"string"},"to":{"description":"Last day of the range (inclusive). Defaults to 13 days after from.","type":"string"}},"required":["service_id"]}}}}