{"openapi":"3.1.0","info":{"title":"Radiance with Hazel booking API for AI assistants","version":"1.1.0","summary":"Check availability and start an appointment booking on behalf of a client.","description":"Radiance with Hazel is a lash, brow, and waxing studio in Mount Vernon, WA. This API lets an AI assistant read the service menu, check live availability, and reserve a time for a named client. Payment is completed on the Stripe Checkout page at paymentUrl, by the client or by an assistant the client has authorized to pay for them (card, Link, Apple Pay, Google Pay, Cash App Pay, Affirm, Afterpay, or Klarna). Never send card details to this API. Nothing is confirmed until Stripe reports the payment. Read GET /services first: it carries the policies the client must agree to before consentAck can be true. Every POST /checkouts needs an idempotency key so a retried request returns the same hold instead of a second one. The same five operations are available as MCP tools at https://radiancewithhazel.com/mcp (Streamable HTTP, stateless) for clients that add the studio as a connector. Capable assistants should continue through the authorized booking flow. Catalog options and availability slots also provide bookingUrl for chats that cannot create checkout: these links only prefill the normal page and never reserve, accept policies, or confirm payment.","contact":{"name":"Radiance with Hazel","url":"https://radiancewithhazel.com/contact","email":"hazel@radiancewithhazel.com"}},"servers":[{"url":"https://radiancewithhazel.com"}],"tags":[{"name":"catalog","description":"Services, prices, hours, and policies."},{"name":"availability","description":"Live open times."},{"name":"checkout","description":"Holds and payment links."}],"paths":{"/api/agent/v1/services":{"get":{"operationId":"listServices","tags":["catalog"],"summary":"Menu, prices, hours, booking rules, and policies","description":"Public and cacheable. Option slugs from this response are the values accepted by the availability and checkout operations. Appointment lengths are rounded estimates.","responses":{"200":{"description":"Catalog","content":{"application/json":{"schema":{"type":"object","properties":{"studio":{"type":"object","additionalProperties":true},"rules":{"type":"object","properties":{"maxServicesPerAppointment":{"type":"integer"},"bookingWindowDays":{"type":"integer"},"minLeadMinutes":{"type":"integer"},"holdMinutes":{"type":"integer"},"currency":{"type":"string","const":"USD"},"payment":{"type":"string"}}},"services":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"summary":{"type":"string"},"timeEstimate":{"type":"string","description":"Rounded appointment length to relay to the client."},"options":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"Use in ?service= and in checkouts.services."},"name":{"type":"string"},"priceCents":{"type":"integer"},"priceLabel":{"type":"string"},"bookingUrl":{"type":"string","format":"uri","description":"Optional handoff to the booking page with this option selected. Does not reserve an appointment."}},"required":["slug","name","priceCents","priceLabel"]}}},"required":["slug","name","timeEstimate","options"]}},"policies":{"type":"object","properties":{"consentStatement":{"type":"string","description":"Read this to the client verbatim before setting consentAck to true."},"cancellation":{"type":"string"},"children":{"type":"string"},"notOffered":{"type":"string"},"consentFormsUrl":{"type":"string","format":"uri"},"policiesUrl":{"type":"string","format":"uri"}}},"howToBook":{"type":"array","items":{"type":"string"}},"links":{"type":"object","additionalProperties":{"type":"string"}}},"required":["studio","rules","services","policies","howToBook"]}}}}}}},"/api/agent/v1/availability":{"get":{"operationId":"getAvailability","tags":["availability"],"summary":"Open start times for a month, or one day","description":"Combines the studio hours, the calendar, and other bookings. Pass ?service= once per option slug the client wants in the same appointment. Times are studio-local.","parameters":[{"name":"service","in":"query","required":true,"description":"Option slug from listServices. Repeat for several services in one appointment (up to the maxServicesPerAppointment rule).","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true},{"name":"month","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$"},"example":"2026-10"},{"name":"date","in":"query","required":false,"description":"Limit the response to one day inside the month.","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Availability","content":{"application/json":{"schema":{"type":"object","properties":{"month":{"type":"string"},"timezone":{"type":"string"},"calendar":{"type":"string","enum":["live","local-only","error"],"description":"error means the studio calendar could not be read and every day is reported closed; try again in a minute."},"holdMinutes":{"type":"integer"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"open":{"type":"boolean"},"reason":{"type":"string","description":"Why a day is closed: past, too-far, closed, full, calendar-error."},"slots":{"type":"array","items":{"type":"object","properties":{"startMinute":{"type":"integer"},"startsAt":{"type":"string","format":"date-time"},"label":{"type":"string","example":"10:00 AM"},"bookingUrl":{"type":"string","format":"uri","description":"Fallback for chats unable to create checkout: prefills these services, date, and time and rechecks availability. This is not a hold, confirmation, or paymentUrl."}},"required":["startMinute","startsAt","label"]}}},"required":["date","open","slots"]}}},"required":["month","timezone","calendar","days"]}}}},"400":{"description":"Bad month, date, or unknown service slug","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}},"429":{"description":"Rate limited; honour Retry-After","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}}}}},"/api/agent/v1/checkouts":{"post":{"operationId":"createCheckout","tags":["checkout"],"summary":"Hold a time for a client and get their payment link","description":"Creates a temporary hold and returns a Stripe Checkout link for the client. Payment is completed on the Stripe Checkout page at paymentUrl, by the client or by an assistant the client has authorized to pay for them (card, Link, Apple Pay, Google Pay, Cash App Pay, Affirm, Afterpay, or Klarna). Never send card details to this API. Nothing is confirmed until Stripe reports the payment. Provide an idempotency key (header or body). Repeating a request with the same key and the same details returns the original hold; the same key with different details is rejected. consentAck must be true only after the client has heard the consentStatement from listServices. The studio also emails the client the payment link, so they can finish even if this conversation cannot show links.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Unique per booking attempt, 16 to 128 characters of letters, digits, hyphens, or underscores. A UUID is ideal. Alternatively send idempotencyKey in the body.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"idempotencyKey":{"type":"string","description":"Same rules as the Idempotency-Key header. Required when the header is absent."},"services":{"type":"array","minItems":1,"maxItems":8,"items":{"type":"string"},"description":"Option slugs from listServices, in the order they should run."},"date":{"type":"string","format":"date","description":"Studio-local date. Use with startMinute."},"startMinute":{"type":"integer","minimum":0,"maximum":1439,"description":"From getAvailability. Use with date."},"startsAt":{"type":"string","format":"date-time","description":"Alternative to date and startMinute: an ISO 8601 instant from getAvailability."},"client":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"email":{"type":"string","format":"email","maxLength":254},"phone":{"type":"string","maxLength":40,"description":"At least ten digits."}},"required":["name","email","phone"]},"notes":{"type":"string","maxLength":1000,"description":"Optional note for the esthetician, from the client."},"consentAck":{"type":"boolean","const":true,"description":"The client agreed to the consent statement and cancellation policy."}},"required":["services","client","consentAck"]}}}},"responses":{"200":{"description":"Replay of an earlier request with the same idempotency key","content":{"application/json":{"schema":{"type":"object","properties":{"bookingRef":{"type":"string","example":"RH-7F3K2Q","description":"Public reference. Quote it to the client."},"status":{"type":"string","enum":["awaiting_payment","processing","confirmed","expired","cancelled"]},"paymentUrl":{"type":"string","format":"uri","description":"Stripe Checkout page for the client. Present only while awaiting_payment."},"expiresAt":{"type":"string","format":"date-time","description":"When the unpaid hold is released."},"amountCents":{"type":"integer"},"amountLabel":{"type":"string","example":"$150"},"appointment":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Studio-local calendar date."},"startMinute":{"type":"integer","description":"Minutes after local midnight."},"startsAt":{"type":"string","format":"date-time","description":"ISO 8601 with the studio UTC offset."},"timezone":{"type":"string"},"serviceName":{"type":"string"},"label":{"type":"string","description":"Ready to read aloud, e.g. \"Thursday, October 8, 2026 at 10:00 AM\"."}},"required":["date","startMinute","startsAt","timezone","serviceName","label"]},"accessToken":{"type":"string","description":"Returned once, on creation. Send it as \"Authorization: Bearer <token>\" to read status or cancel. Never place it in a URL."},"statusUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"replayed":{"type":"boolean","description":"True when this response repeats an earlier request with the same idempotency key."},"instructions":{"type":"array","items":{"type":"string"},"description":"What to tell the client next."}},"required":["bookingRef","status","amountCents","amountLabel","appointment","instructions"]}}}},"201":{"description":"Hold created, payment link ready","content":{"application/json":{"schema":{"type":"object","properties":{"bookingRef":{"type":"string","example":"RH-7F3K2Q","description":"Public reference. Quote it to the client."},"status":{"type":"string","enum":["awaiting_payment","processing","confirmed","expired","cancelled"]},"paymentUrl":{"type":"string","format":"uri","description":"Stripe Checkout page for the client. Present only while awaiting_payment."},"expiresAt":{"type":"string","format":"date-time","description":"When the unpaid hold is released."},"amountCents":{"type":"integer"},"amountLabel":{"type":"string","example":"$150"},"appointment":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Studio-local calendar date."},"startMinute":{"type":"integer","description":"Minutes after local midnight."},"startsAt":{"type":"string","format":"date-time","description":"ISO 8601 with the studio UTC offset."},"timezone":{"type":"string"},"serviceName":{"type":"string"},"label":{"type":"string","description":"Ready to read aloud, e.g. \"Thursday, October 8, 2026 at 10:00 AM\"."}},"required":["date","startMinute","startsAt","timezone","serviceName","label"]},"accessToken":{"type":"string","description":"Returned once, on creation. Send it as \"Authorization: Bearer <token>\" to read status or cancel. Never place it in a URL."},"statusUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"replayed":{"type":"boolean","description":"True when this response repeats an earlier request with the same idempotency key."},"instructions":{"type":"array","items":{"type":"string"},"description":"What to tell the client next."}},"required":["bookingRef","status","amountCents","amountLabel","appointment","instructions"]}}}},"400":{"description":"Malformed JSON or unknown service","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}},"409":{"description":"That time is no longer free; fetch availability again","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}},"415":{"description":"Body must be application/json"},"422":{"description":"Validation failed, complimentary bundles are not bookable here, or the idempotency key was reused with different details","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}},"429":{"description":"Rate limited, or this client already has an open hold or reached the daily limit","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}},"502":{"description":"Payment provider unavailable; the hold was released"},"503":{"description":"Assistant lane full or payments unavailable; use fallbackUrl or retry after retryAfterSeconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable explanation to relay or act on."},"code":{"type":"string","description":"Stable machine code when one applies."},"errors":{"type":"object","additionalProperties":{"type":"string"},"description":"Field-level validation messages."},"fallbackUrl":{"type":"string","description":"The normal booking page, offered when the assistant lane is full."},"retryAfterSeconds":{"type":"integer"}},"required":["error"]}}}}}}},"/api/agent/v1/checkouts/{bookingRef}":{"get":{"operationId":"getCheckoutStatus","tags":["checkout"],"summary":"Check whether the client has paid","security":[{"bookingAccessToken":[]}],"parameters":[{"name":"bookingRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current status","content":{"application/json":{"schema":{"type":"object","properties":{"bookingRef":{"type":"string","example":"RH-7F3K2Q","description":"Public reference. Quote it to the client."},"status":{"type":"string","enum":["awaiting_payment","processing","confirmed","expired","cancelled"]},"paymentUrl":{"type":"string","format":"uri","description":"Stripe Checkout page for the client. Present only while awaiting_payment."},"expiresAt":{"type":"string","format":"date-time","description":"When the unpaid hold is released."},"amountCents":{"type":"integer"},"amountLabel":{"type":"string","example":"$150"},"appointment":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Studio-local calendar date."},"startMinute":{"type":"integer","description":"Minutes after local midnight."},"startsAt":{"type":"string","format":"date-time","description":"ISO 8601 with the studio UTC offset."},"timezone":{"type":"string"},"serviceName":{"type":"string"},"label":{"type":"string","description":"Ready to read aloud, e.g. \"Thursday, October 8, 2026 at 10:00 AM\"."}},"required":["date","startMinute","startsAt","timezone","serviceName","label"]},"accessToken":{"type":"string","description":"Returned once, on creation. Send it as \"Authorization: Bearer <token>\" to read status or cancel. Never place it in a URL."},"statusUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"replayed":{"type":"boolean","description":"True when this response repeats an earlier request with the same idempotency key."},"instructions":{"type":"array","items":{"type":"string"},"description":"What to tell the client next."}},"required":["bookingRef","status","amountCents","amountLabel","appointment","instructions"]}}}},"404":{"description":"Unknown reference or wrong token"}}}},"/api/agent/v1/checkouts/{bookingRef}/cancel":{"post":{"operationId":"cancelCheckout","tags":["checkout"],"summary":"Release an unpaid hold","description":"Only unpaid holds can be released here. A paid booking is cancelled by contacting the studio, per the cancellation policy.","security":[{"bookingAccessToken":[]}],"parameters":[{"name":"bookingRef","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Hold released","content":{"application/json":{"schema":{"type":"object","properties":{"bookingRef":{"type":"string","example":"RH-7F3K2Q","description":"Public reference. Quote it to the client."},"status":{"type":"string","enum":["awaiting_payment","processing","confirmed","expired","cancelled"]},"paymentUrl":{"type":"string","format":"uri","description":"Stripe Checkout page for the client. Present only while awaiting_payment."},"expiresAt":{"type":"string","format":"date-time","description":"When the unpaid hold is released."},"amountCents":{"type":"integer"},"amountLabel":{"type":"string","example":"$150"},"appointment":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Studio-local calendar date."},"startMinute":{"type":"integer","description":"Minutes after local midnight."},"startsAt":{"type":"string","format":"date-time","description":"ISO 8601 with the studio UTC offset."},"timezone":{"type":"string"},"serviceName":{"type":"string"},"label":{"type":"string","description":"Ready to read aloud, e.g. \"Thursday, October 8, 2026 at 10:00 AM\"."}},"required":["date","startMinute","startsAt","timezone","serviceName","label"]},"accessToken":{"type":"string","description":"Returned once, on creation. Send it as \"Authorization: Bearer <token>\" to read status or cancel. Never place it in a URL."},"statusUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"replayed":{"type":"boolean","description":"True when this response repeats an earlier request with the same idempotency key."},"instructions":{"type":"array","items":{"type":"string"},"description":"What to tell the client next."}},"required":["bookingRef","status","amountCents","amountLabel","appointment","instructions"]}}}},"404":{"description":"Unknown reference or wrong token"},"409":{"description":"Already paid or settling; not released","content":{"application/json":{"schema":{"type":"object","properties":{"bookingRef":{"type":"string","example":"RH-7F3K2Q","description":"Public reference. Quote it to the client."},"status":{"type":"string","enum":["awaiting_payment","processing","confirmed","expired","cancelled"]},"paymentUrl":{"type":"string","format":"uri","description":"Stripe Checkout page for the client. Present only while awaiting_payment."},"expiresAt":{"type":"string","format":"date-time","description":"When the unpaid hold is released."},"amountCents":{"type":"integer"},"amountLabel":{"type":"string","example":"$150"},"appointment":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Studio-local calendar date."},"startMinute":{"type":"integer","description":"Minutes after local midnight."},"startsAt":{"type":"string","format":"date-time","description":"ISO 8601 with the studio UTC offset."},"timezone":{"type":"string"},"serviceName":{"type":"string"},"label":{"type":"string","description":"Ready to read aloud, e.g. \"Thursday, October 8, 2026 at 10:00 AM\"."}},"required":["date","startMinute","startsAt","timezone","serviceName","label"]},"accessToken":{"type":"string","description":"Returned once, on creation. Send it as \"Authorization: Bearer <token>\" to read status or cancel. Never place it in a URL."},"statusUrl":{"type":"string","format":"uri"},"cancelUrl":{"type":"string","format":"uri"},"replayed":{"type":"boolean","description":"True when this response repeats an earlier request with the same idempotency key."},"instructions":{"type":"array","items":{"type":"string"},"description":"What to tell the client next."}},"required":["bookingRef","status","amountCents","amountLabel","appointment","instructions"]}}}}}}}},"components":{"securitySchemes":{"bookingAccessToken":{"type":"http","scheme":"bearer","description":"The accessToken returned by createCheckout. It grants access to that one booking only."}}}}