{"openapi":"3.1.0","info":{"title":"Bookatu API","version":"1.0.0","description":"The public Bookatu REST API for booking and managing appointments. Public endpoints (business, services, staff, availability, book) need no auth; the customer booking-management endpoints under /booking/{id} are authorised by the appointment id in the path; the rest require an API key (Authorization: Bearer bk_live_… or X-Api-Key). Generated from the same source as the human docs at https://bookatu.com/developers.","contact":{"name":"Bookatu support","url":"https://bookatu.com/developers"}},"servers":[{"url":"https://bookatu.com","description":"Production"}],"tags":[{"name":"Public","description":"No authentication required."},{"name":"Booking id","description":"No API key. Authorised by the unguessable appointment id in the path, which is only ever given to the person who booked."},{"name":"Authenticated","description":"Requires an API key scoped to the business."}],"paths":{"/api/v1/{slug}/business":{"get":{"operationId":"business","summary":"Business profile + opening hours","description":"The business profile: name, industry, country, currency, timezone, locale, address, phone, the public booking link, and the full week of opening hours. Lets an agent introduce the business and know when it is open before checking availability. No authentication required.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"Business profile + opening hours","content":{"application/json":{}},"x-example-response":"{\n  \"name\": \"Parnell Nails\",\n  \"slug\": \"parnell-nails\",\n  \"industry\": \"nails\",\n  \"country\": \"NZ\",\n  \"currency\": \"NZD\",\n  \"timezone\": \"Pacific/Auckland\",\n  \"locale\": \"en-NZ\",\n  \"address\": \"12 Queen St, Auckland\",\n  \"phone\": \"09 555 0100\",\n  \"bookingUrl\": \"https://bookatu.com/parnell-nails/book\",\n  \"hours\": [\n    { \"weekday\": 0, \"open\": false, \"openMin\": null, \"closeMin\": null, \"breakStartMin\": null, \"breakEndMin\": null },\n    { \"weekday\": 1, \"open\": true,  \"openMin\": 540,  \"closeMin\": 1080, \"breakStartMin\": 780, \"breakEndMin\": 840 },\n    { \"weekday\": 2, \"open\": true,  \"openMin\": 540,  \"closeMin\": 1080, \"breakStartMin\": null, \"breakEndMin\": null },\n    { \"weekday\": 3, \"open\": true,  \"openMin\": 540,  \"closeMin\": 1080, \"breakStartMin\": null, \"breakEndMin\": null },\n    { \"weekday\": 4, \"open\": true,  \"openMin\": 540,  \"closeMin\": 1080, \"breakStartMin\": null, \"breakEndMin\": null },\n    { \"weekday\": 5, \"open\": true,  \"openMin\": 540,  \"closeMin\": 1140, \"breakStartMin\": null, \"breakEndMin\": null },\n    { \"weekday\": 6, \"open\": true,  \"openMin\": 600,  \"closeMin\": 960,  \"breakStartMin\": null, \"breakEndMin\": null }\n  ]\n}\n\n// hours always has all 7 days. weekday 0 = Sunday .. 6 = Saturday.\n// openMin / closeMin are minutes from local midnight, and are null when closed.\n// breakStartMin / breakEndMin are a mid-day closure (Monday above shuts 1pm to\n// 2pm) and are null when the day runs straight through. A day with a break\n// trades openMin-breakStartMin and breakEndMin-closeMin, and NOTHING between:\n// treat the outer pair alone as open all day and you will send someone to a\n// locked door."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error (details are never leaked)."}}}},"/api/v1/{slug}/services":{"get":{"operationId":"services","summary":"List active, online-bookable services","description":"The org's public profile plus every service that is active and bookable online, ordered by sort order then name. Use a service id and durationMin when checking availability and booking. No authentication required.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"List active, online-bookable services","content":{"application/json":{}},"x-example-response":"{\n  \"org\": {\n    \"name\": \"Parnell Nails\",\n    \"slug\": \"parnell-nails\",\n    \"currency\": \"NZD\",\n    \"timezone\": \"Pacific/Auckland\"\n  },\n  \"services\": [\n    {\n      \"id\": \"svc_abc123\",\n      \"name\": \"Gel Manicure\",\n      \"description\": \"Long-lasting gel polish with cuticle care.\",\n      \"category\": \"Nails\",\n      \"durationMin\": 60,\n      \"priceCents\": 7500,\n      \"currency\": \"NZD\"\n    }\n  ]\n}\n\n// description is the service description, or null when none is set."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error."}}}},"/api/v1/{slug}/staff":{"get":{"operationId":"staff","summary":"List bookable team members","description":"The bookable team members (id, name, title, bio) so an agent can offer \"book with X\". Public-facing fields only — never staff email or phone. No authentication required.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"List bookable team members","content":{"application/json":{}},"x-example-response":"{\n  \"org\": { \"name\": \"Parnell Nails\", \"slug\": \"parnell-nails\", \"timezone\": \"Pacific/Auckland\" },\n  \"staff\": [\n    { \"id\": \"stf_abc\", \"name\": \"Anna\", \"title\": \"Senior Nail Tech\", \"bio\": null }\n  ]\n}\n\n// title and bio are null when not set."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error."}}}},"/api/v1/{slug}/availability":{"get":{"operationId":"availability","summary":"Open slots for a service","description":"Bookable slots for a service across a date window. The engine accounts for opening hours, existing appointments, buffer times, resources and minimum booking notice. Each slot's startAt is a UTC ISO 8601 string ready to pass straight to a booking call. No authentication required. This is the read for a NEW booking: to move an existing one, use GET /booking/{id}/availability instead, because this read counts that booking's own slot as busy.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"serviceId","in":"query","required":true,"schema":{"type":"string"},"description":"An active, online-bookable service id."},{"name":"from","in":"query","required":true,"schema":{"type":"string"},"description":"Start date, YYYY-MM-DD."},{"name":"to","in":"query","required":true,"schema":{"type":"string"},"description":"End date, YYYY-MM-DD. Must be on or after from, and at most 60 days later."},{"name":"staffId","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict slots to one team member."},{"name":"partySize","in":"query","required":false,"schema":{"type":"number"},"description":"How many people are coming, 1–50. Only times with a table that seats them come back, and each slot then names the tables that fit (resourceIds), smallest first — pass one straight back as resourceId when booking. Omit to apply no capacity filter."}],"responses":{"200":{"description":"Open slots for a service","content":{"application/json":{}},"x-example-response":"{\n  \"org\": { \"name\": \"Parnell Nails\", \"slug\": \"parnell-nails\", \"timezone\": \"Pacific/Auckland\" },\n  \"days\": [\n    {\n      \"date\": \"2026-06-10\",\n      \"open\": true,\n      \"slots\": [\n        { \"startMin\": 540, \"startAt\": \"2026-06-09T21:00:00.000Z\" }\n      ]\n    },\n    { \"date\": \"2026-06-11\", \"open\": false, \"slots\": [] }\n  ]\n}\n\n// startMin is minutes from local midnight; startAt is the UTC instant to book."},"400":{"description":"serviceId is missing, from/to are not YYYY-MM-DD, from is later than to, or the range exceeds 60 days."},"404":{"description":"The {slug} is unknown, or the serviceId is not an active, online-bookable service in this org."},"500":{"description":"Unexpected server error."}}}},"/api/v1/{slug}/book":{"post":{"operationId":"book","summary":"Create a booking (online source)","description":"The customer-facing booking call. The slot is validated server-side (race-condition safe), a customer record is matched by email then phone or created, and a booking reference is returned. As an online-source booking it fully respects availability and minimum notice. When a deposit is required the booking is pending and the response includes a payLink to surface to the customer. No authentication required.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"Create a booking (online source)","content":{"application/json":{}},"x-example-response":"{\n  \"ok\": true,\n  \"ref\": \"VRD-7QK2M9\",\n  \"appointmentId\": \"apt_abc123\",\n  \"status\": \"confirmed\",\n  \"needsPayment\": false\n}\n\n// When a deposit is required, status is \"pending\" and a payLink is returned:\n{\n  \"ok\": true,\n  \"ref\": \"VRD-7QK2M9\",\n  \"appointmentId\": \"apt_abc123\",\n  \"status\": \"pending\",\n  \"needsPayment\": true,\n  \"payLink\": \"https://bookatu.com/your-salon/book/success?apt=apt_abc123\"\n}"},"400":{"description":"The request body is not valid JSON."},"404":{"description":"No business exists for the {slug}."},"409":{"description":"The slot was just taken, or the service is unavailable / inactive."},"422":{"description":"Validation failed — the response includes a fieldErrors map naming each invalid field (e.g. email, phone). Also returned when startAt is not a parseable ISO 8601 date-time."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["serviceId","startAt","name","email","phone"],"properties":{"serviceId":{"type":"string","description":"An active, online-bookable service id from GET /services."},"startAt":{"type":"string","description":"ISO 8601 date-time, e.g. a startAt value from GET /availability (\"2026-06-10T09:00:00Z\")."},"name":{"type":"string","description":"Customer name, 2–80 characters (trimmed)."},"email":{"type":"string","description":"A valid email address."},"phone":{"type":"string","description":"6–30 characters; digits, spaces and + ( ) - only."},"staffId":{"type":"string","description":"Specific staff id. Omit to let the system assign a free team member."},"notes":{"type":"string","description":"Free text, max 500 characters."},"partySize":{"type":"number","description":"How many people are coming, 1–50. Restaurants: only a table that seats the party is used, and the booking records the covers. Omit for a booking for one seat, which is every appointment-style booking."},"resourceId":{"type":"string","description":"A specific table (or room) of this business to prefer. Honoured only while it is genuinely free for that time; otherwise the smallest one that fits the party is assigned. An id that is not this business's is ignored."}}}}}}}},"/api/v1/{slug}/booking/{id}":{"get":{"operationId":"booking-get","summary":"Read one booking (the customer's own)","description":"The JSON twin of the manage-booking page a customer reaches from their confirmation email: their booking, its service, its team member, its price and deposit, plus the business's cancellation policy. Authorised by the appointment id alone — no API key, no account — and deliberately narrow: only what the person who booked already knows. No customer record, no notes, no other bookings. Rate limited to 30 requests per IP every 5 minutes.","tags":["Booking id"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id, as returned by POST /book (apt_…)."}],"responses":{"200":{"description":"Read one booking (the customer's own)","content":{"application/json":{}},"x-example-response":"{\n  \"org\": {\n    \"slug\": \"parnell-nails\",\n    \"name\": \"Parnell Nails\",\n    \"currency\": \"NZD\",\n    \"timezone\": \"Pacific/Auckland\"\n  },\n  \"booking\": {\n    \"id\": \"apt_abc123\",\n    \"ref\": \"VRD-7QK2M9\",\n    \"status\": \"confirmed\",\n    \"startAt\": \"2026-06-10T21:00:00.000Z\",\n    \"endAt\": \"2026-06-10T22:00:00.000Z\",\n    \"durationMin\": 60,\n    \"service\": \"Gel Manicure\",\n    \"staff\": \"Anna\",\n    \"priceCents\": 7500,\n    \"depositCents\": 2000,\n    \"addOns\": [{ \"name\": \"Nail art\", \"priceCents\": 1500 }]\n  },\n  \"policy\": {\n    \"cancellationWindowHours\": 24,\n    \"forfeitDepositOnLateCancel\": true\n  }\n}\n\n// policy is the business's cancellation policy: cancelling or moving more than\n// cancellationWindowHours before the start is free, and inside that window a\n// paid deposit is kept when forfeitDepositOnLateCancel is true. ref and staff\n// are null when unset; addOns is [] when there are none."},"404":{"description":"No business exists for the {slug}, or no booking with that id in this business (the same 404 either way — a distinct response would confirm an id exists somewhere)."},"429":{"description":"More than 30 requests from one IP in 5 minutes. The response carries Retry-After."},"500":{"description":"Unexpected server error (details are never leaked)."}}}},"/api/v1/{slug}/booking/{id}/availability":{"get":{"operationId":"booking-reschedule-availability","summary":"Open slots for MOVING this booking","description":"Bookable slots for moving an existing booking, across a date window. This differs from GET /availability in two ways that matter: the booking's own slot is EXCLUDED from the busy set (otherwise an appointment's own hold hides the very time its owner is trying to shift around), and the fit uses the booking's own service and its own length — which is not always the service's default, because an add-on or a duration override can make the booking longer. Pass a returned startAt straight to POST /booking/{id}/reschedule. Authorised by the appointment id alone. Rate limited to 30 requests per IP every 5 minutes.","tags":["Booking id"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id being moved."},{"name":"from","in":"query","required":true,"schema":{"type":"string"},"description":"Start date, YYYY-MM-DD."},{"name":"to","in":"query","required":true,"schema":{"type":"string"},"description":"End date, YYYY-MM-DD. Must be on or after from, and at most 60 days later."}],"responses":{"200":{"description":"Open slots for MOVING this booking","content":{"application/json":{}},"x-example-response":"{\n  \"org\": { \"slug\": \"parnell-nails\", \"name\": \"Parnell Nails\", \"timezone\": \"Pacific/Auckland\" },\n  \"booking\": { \"id\": \"apt_abc123\", \"durationMin\": 60 },\n  \"days\": [\n    {\n      \"date\": \"2026-06-10\",\n      \"open\": true,\n      \"slots\": [\n        { \"startMin\": 540, \"startAt\": \"2026-06-09T21:00:00.000Z\" }\n      ]\n    },\n    { \"date\": \"2026-06-11\", \"open\": false, \"slots\": [] }\n  ]\n}\n\n// booking.durationMin is the length being fitted (the booking's, not the\n// service's default). startMin is minutes from local midnight; startAt is the\n// UTC instant to send back when rescheduling."},"400":{"description":"from/to are not YYYY-MM-DD, from is later than to, or the range exceeds 60 days."},"404":{"description":"No business exists for the {slug}, or no booking with that id in this business."},"429":{"description":"More than 30 requests from one IP in 5 minutes."},"500":{"description":"Unexpected server error (details are never leaked)."}}}},"/api/v1/{slug}/booking/{id}/reschedule":{"post":{"operationId":"booking-reschedule","summary":"Move a booking to a new time","description":"The customer-facing reschedule — the same call the manage-booking web page makes, with the same strict rules. The new time is validated as a real, open slot: opening hours, existing bookings, buffers, resources and the business's minimum booking notice all apply, and the booking's own hold is excluded so it can be moved within its own slot. A time that is not bookable returns 409 with the reason, and nothing changes. The booking keeps its team member unless the availability engine has to reassign it, and its length is unchanged. Moving a booking fires the booking.rescheduled webhook and updates any connected calendar. Only an upcoming pending or confirmed booking can be moved: a cancelled, completed, no-show or past booking returns 409 — the same rule as the web page, so a cancelled booking can never be quietly reinstated. Rate limited to 10 requests per IP every 10 minutes.","tags":["Booking id"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id to move."}],"responses":{"200":{"description":"Move a booking to a new time","content":{"application/json":{}},"x-example-response":"{ \"ok\": true, \"startAt\": \"2026-06-12T21:00:00.000Z\" }\n\n// startAt echoes the time that was taken, so the new booking can be rendered\n// without a second read."},"400":{"description":"The request body is not valid JSON."},"404":{"description":"No business exists for the {slug}, or no booking with that id in this business."},"409":{"description":"The time is not bookable (already taken, outside opening hours, or inside the minimum booking notice) — the response carries the reason. Also returned when the booking is cancelled, completed, a no-show, or has already started."},"422":{"description":"startAt is missing, is not a parseable ISO 8601 date-time, or is in the past."},"429":{"description":"More than 10 requests from one IP in 10 minutes. The response carries Retry-After."},"500":{"description":"Unexpected server error (details are never leaked)."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["startAt"],"properties":{"startAt":{"type":"string","description":"The new start time as an ISO 8601 date-time, e.g. a startAt from GET /booking/{id}/availability. Must be in the future."}}}}}}}},"/api/v1/{slug}/booking/{id}/cancel":{"post":{"operationId":"booking-cancel","summary":"Cancel a booking (the customer's own)","description":"The customer-facing cancellation — the same call the manage-booking web page makes, so the cancellation policy, any deposit refund or forfeiture, the booking.cancelled webhook and the waitlist re-offer are identical however it is triggered. The forfeited flag says whether a paid deposit was kept under the policy, which an app needs in order to tell the person the truth on the confirmation screen. Cancelling an already-cancelled booking is idempotent (alreadyCancelled: true), so a retry after a dropped connection is safe. Rate limited to 10 requests per IP every 10 minutes.","tags":["Booking id"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id to cancel."}],"responses":{"200":{"description":"Cancel a booking (the customer's own)","content":{"application/json":{}},"x-example-response":"{ \"ok\": true, \"alreadyCancelled\": false, \"forfeited\": false }\n\n// forfeited is true when a paid deposit was kept under the cancellation policy\n// (see policy on GET /booking/{id})."},"404":{"description":"No business exists for the {slug}, or no booking with that id in this business."},"409":{"description":"The booking could not be cancelled — the response carries the reason."},"429":{"description":"More than 10 requests from one IP in 10 minutes. The response carries Retry-After."},"500":{"description":"Unexpected server error (details are never leaked)."}}}},"/api/v1/{slug}/customers":{"get":{"operationId":"customers","summary":"List customers","description":"Up to 1000 customers in this org (id, name, email, phone), ordered by name. Requires a valid API key whose org matches the {slug}.","tags":["Authenticated"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"List customers","content":{"application/json":{}},"x-example-response":"{\n  \"customers\": [\n    { \"id\": \"cus_xyz\", \"name\": \"Alex Smith\", \"email\": \"alex@example.com\", \"phone\": \"+64211234567\" }\n  ]\n}\n\n// Capped at 1000 customers, ordered by name."},"401":{"description":"The API key is missing or invalid."},"403":{"description":"The key belongs to a different organisation."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error."}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/api/v1/{slug}/appointments":{"get":{"operationId":"appointments","summary":"List appointments","description":"By default, upcoming appointments with status pending or confirmed whose startAt is now or later, ordered by start time, capped at 200. Optional filters let you pull a specific window or statuses, and limit/offset page through results — when more rows may exist the response includes nextOffset. Requires a valid API key scoped to this org.","tags":["Authenticated"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"from","in":"query","required":false,"schema":{"type":"string"},"description":"Lower time bound, ISO 8601. Overrides the default \"now\" — use it to list a past window."},{"name":"to","in":"query","required":false,"schema":{"type":"string"},"description":"Upper time bound, ISO 8601 (inclusive)."},{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated statuses to include: pending, confirmed, completed, cancelled, no_show. Defaults to pending + confirmed."},{"name":"limit","in":"query","required":false,"schema":{"type":"number"},"description":"Page size, 1–200 (default 200)."},{"name":"offset","in":"query","required":false,"schema":{"type":"number"},"description":"Rows to skip, for pagination. The response carries nextOffset when more may exist."}],"responses":{"200":{"description":"List appointments","content":{"application/json":{}},"x-example-response":"{\n  \"appointments\": [\n    {\n      \"id\": \"apt_abc123\",\n      \"ref\": \"VRD-7QK2M9\",\n      \"serviceName\": \"Gel Manicure\",\n      \"startAt\": \"2026-06-10T21:00:00.000Z\",\n      \"endAt\": \"2026-06-10T22:00:00.000Z\",\n      \"durationMin\": 60,\n      \"priceCents\": 7500,\n      \"status\": \"confirmed\",\n      \"customerId\": \"cus_xyz\",\n      \"staffId\": \"stf_abc\",\n      \"notes\": null,\n      \"createdAt\": \"2026-06-01T03:12:00.000Z\"\n    }\n  ]\n}\n\n// Only pending + confirmed, startAt >= now, oldest first, max 200 rows."},"401":{"description":"The API key is missing or invalid."},"403":{"description":"The key belongs to a different organisation."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error."}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]},"post":{"operationId":"appointments-create","summary":"Create a booking (admin source)","description":"Create a booking with source: admin and the same body as POST /book. An admin-source booking deliberately bypasses online restrictions: minimum booking notice is not enforced, and — unlike POST /book — a slot conflict does NOT reject the request. The availability engine is used only to resolve a free staff member and resource; it does not block the booking. The one guard that still applies is the database uniqueness constraint on (staff, start time), which returns 409 if that exact staff member is already booked at that instant. Use this for trusted staff-side tools and agents acting on behalf of the business; use POST /book for customer-facing flows that must respect availability. Requires a valid API key scoped to this org.","tags":["Authenticated"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."}],"responses":{"200":{"description":"Create a booking (admin source)","content":{"application/json":{}},"x-example-response":"{\n  \"ok\": true,\n  \"ref\": \"VRD-7QK2M9\",\n  \"appointmentId\": \"apt_abc123\",\n  \"status\": \"confirmed\",\n  \"needsPayment\": false\n}\n\n// When a deposit is required, status is \"pending\" and a payLink is returned:\n{\n  \"ok\": true,\n  \"ref\": \"VRD-7QK2M9\",\n  \"appointmentId\": \"apt_abc123\",\n  \"status\": \"pending\",\n  \"needsPayment\": true,\n  \"payLink\": \"https://bookatu.com/your-salon/book/success?apt=apt_abc123\"\n}"},"400":{"description":"The request body is not valid JSON."},"401":{"description":"The API key is missing or invalid."},"403":{"description":"The key belongs to a different organisation, or the key is read-only (creating a booking needs a read-write key)."},"404":{"description":"No business exists for the {slug}."},"409":{"description":"The service is unavailable / inactive, or that exact staff member is already booked at that instant (database uniqueness guard)."},"422":{"description":"Validation failed — the response includes a fieldErrors map naming each invalid field (e.g. email, phone). Also returned when startAt is not a parseable ISO 8601 date-time."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["serviceId","startAt","name","email","phone"],"properties":{"serviceId":{"type":"string","description":"An active, online-bookable service id from GET /services."},"startAt":{"type":"string","description":"ISO 8601 date-time, e.g. a startAt value from GET /availability (\"2026-06-10T09:00:00Z\")."},"name":{"type":"string","description":"Customer name, 2–80 characters (trimmed)."},"email":{"type":"string","description":"A valid email address."},"phone":{"type":"string","description":"6–30 characters; digits, spaces and + ( ) - only."},"staffId":{"type":"string","description":"Specific staff id. Omit to let the system assign a free team member."},"notes":{"type":"string","description":"Free text, max 500 characters."},"partySize":{"type":"number","description":"How many people are coming, 1–50. Restaurants: only a table that seats the party is used, and the booking records the covers. Omit for a booking for one seat, which is every appointment-style booking."},"resourceId":{"type":"string","description":"A specific table (or room) of this business to prefer. Honoured only while it is genuinely free for that time; otherwise the smallest one that fits the party is assigned. An id that is not this business's is ignored."}}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/api/v1/{slug}/services/{id}":{"get":{"operationId":"services-get","summary":"Get one service","description":"A single active, online-bookable service by id — the same shape as a row of GET /services. No authentication required.","tags":["Public"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The service id (from GET /services)."}],"responses":{"200":{"description":"Get one service","content":{"application/json":{}},"x-example-response":"{\n  \"id\": \"svc_abc123\",\n  \"name\": \"Gel Manicure\",\n  \"description\": \"Long-lasting gel polish with cuticle care.\",\n  \"category\": \"Nails\",\n  \"durationMin\": 60,\n  \"priceCents\": 7500,\n  \"currency\": \"NZD\"\n}"},"404":{"description":"No business exists for the {slug}, or no active, online-bookable service with that id."},"500":{"description":"Unexpected server error."}}}},"/api/v1/{slug}/appointments/{id}":{"get":{"operationId":"appointments-get","summary":"Get one appointment","description":"A single appointment by id, scoped to the key's org (a key for one business can never read another's). Same lean shape as a row of GET /appointments. A read-only key may call this. Requires a valid API key scoped to this org.","tags":["Authenticated"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id."}],"responses":{"200":{"description":"Get one appointment","content":{"application/json":{}},"x-example-response":"{\n  \"id\": \"apt_abc123\",\n  \"ref\": \"VRD-7QK2M9\",\n  \"serviceName\": \"Gel Manicure\",\n  \"startAt\": \"2026-06-10T21:00:00.000Z\",\n  \"endAt\": \"2026-06-10T22:00:00.000Z\",\n  \"durationMin\": 60,\n  \"priceCents\": 7500,\n  \"status\": \"confirmed\",\n  \"customerId\": \"cus_xyz\",\n  \"staffId\": \"stf_abc\",\n  \"notes\": null,\n  \"createdAt\": \"2026-06-01T03:12:00.000Z\"\n}"},"401":{"description":"The API key is missing or invalid."},"403":{"description":"The key belongs to a different organisation."},"404":{"description":"No business exists for the {slug}, or no appointment with that id in this org."},"500":{"description":"Unexpected server error."}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}},"/api/v1/{slug}/appointments/{id}/cancel":{"post":{"operationId":"appointments-cancel","summary":"Cancel a booking","description":"Cancel a booking. Scoped to the key's org, so a key for one business can never touch another's data. Fires the booking.cancelled webhook and re-offers the freed slot to the waitlist. The forfeited flag is true when a paid deposit was forfeited under the cancellation policy. Cancelling an already-cancelled booking is idempotent. Requires a valid API key scoped to this org.","tags":["Authenticated"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Your business slug."},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The appointment id to cancel."}],"responses":{"200":{"description":"Cancel a booking","content":{"application/json":{}},"x-example-response":"{ \"ok\": true, \"id\": \"apt_abc123\", \"forfeited\": false }"},"400":{"description":"The booking could not be cancelled (e.g. the appointment id is unknown in this org)."},"401":{"description":"The API key is missing or invalid."},"403":{"description":"The key belongs to a different organisation, or the key is read-only (cancelling needs a read-write key)."},"404":{"description":"No business exists for the {slug}."},"500":{"description":"Unexpected server error."}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"reason":{"type":"string","description":"Optional cancellation reason, truncated to 200 characters and stored on the booking."}}}}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Your API key as a Bearer token: Authorization: Bearer bk_live_…"},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Your API key in the X-Api-Key header."}}}}