Bookatu API
Every Bookatu business exposes a REST API, signed webhooks, and a Model Context Protocol (MCP) server. Use them to build integrations, booking automations, and AI-powered scheduling agents.
Authentication
The public endpoints (business, services, staff, availability, book) require no authentication and can be called from any origin. The key-protected endpoints (customers, appointments) and the MCP server need an API key issued from your admin dashboard at /{slug}/admin/api.
Pass the key as a Bearer token in the Authorization header, or in the X-Api-Key header.
Authorization: Bearer bk_live_YOUR_KEY X-Api-Key: bk_live_YOUR_KEY
- Keys are issued from the admin dashboard and shown exactly once at creation. Store them in a secret manager or environment variable; they cannot be retrieved again.
- A key is a random token (bk_live_ followed by a 32-character id). Only its SHA-256 hash is stored, and verification uses a timing-safe comparison.
- Keys support an optional expiry, are checked against revocation on every request, and record a lastUsedAt timestamp so you can audit usage.
- Revoke a key immediately from the dashboard if it is ever exposed. Revoked and expired keys stop authenticating at once.
Keep keys server-side
Every v1 route is CORS-open, so a browser can reach these URLs. Never embed a bk_live_ key in browser or mobile-client code — call key-protected endpoints from your own server.
Base URL & CORS
https://bookatu.com/api/v1/{slug}Replace {slug} with the business URL slug visible in your admin and public booking link. All responses are application/json. Errors always return { ok: false, error: "..." } (validation errors also include a fieldErrors map). Stack traces are never leaked.
Every v1 endpoint returns Access-Control-Allow-Origin: * and answers the CORS preflight (OPTIONS) with 204. Key-protected endpoints additionally allow the Authorization and X-Api-Key request headers. Because any origin can reach these routes, never embed a bk_live_ key in browser or mobile-client code — call key-protected endpoints from your own server.
| Endpoint | Auth | Description |
|---|---|---|
| GET /business | None | Business profile + opening hours |
| GET /services | None | List active, online-bookable services |
| GET /staff | None | List bookable team members |
| GET /availability | None | Open slots for a service |
| POST /book | None | Create a booking (online source) |
| GET /customers | API key | List customers |
| GET /appointments | API key | List upcoming appointments |
| POST /appointments | API key | Create a booking (admin source) |
| POST /appointments/{id}/cancel | API key | Cancel a booking |
A note on the mobile API
A separate, first-party mobile API (under /api/mobile) powers the Bookatu mobile experience. It is actively evolving and is intentionally not documented here, as its endpoints are still changing. For integrations, build against the v1 REST API or the MCP server documented on this page.
Endpoint reference
Across responses, a booking status is one of: pending, confirmed, completed, cancelled, no_show.
GET /business
PublicGET /api/v1/{slug}/businessBusiness profile + opening hours
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.
curl https://bookatu.com/api/v1/your-salon/business
Response example
{
"name": "Parnell Nails",
"slug": "parnell-nails",
"industry": "nails",
"country": "NZ",
"currency": "NZD",
"timezone": "Pacific/Auckland",
"locale": "en-NZ",
"address": "12 Queen St, Auckland",
"phone": "09 555 0100",
"bookingUrl": "https://bookatu.com/parnell-nails/book",
"hours": [
{ "weekday": 0, "open": false, "openMin": null, "closeMin": null },
{ "weekday": 1, "open": true, "openMin": 540, "closeMin": 1080 },
{ "weekday": 2, "open": true, "openMin": 540, "closeMin": 1080 },
{ "weekday": 3, "open": true, "openMin": 540, "closeMin": 1080 },
{ "weekday": 4, "open": true, "openMin": 540, "closeMin": 1080 },
{ "weekday": 5, "open": true, "openMin": 540, "closeMin": 1140 },
{ "weekday": 6, "open": true, "openMin": 600, "closeMin": 960 }
]
}
// hours always has all 7 days. weekday 0 = Sunday .. 6 = Saturday.
// openMin / closeMin are minutes from local midnight, and are null when closed.Errors
| Status | When |
|---|---|
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error (details are never leaked). |
GET /services
PublicGET /api/v1/{slug}/servicesList active, online-bookable services
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.
curl https://bookatu.com/api/v1/your-salon/services
Response example
{
"org": {
"name": "Parnell Nails",
"slug": "parnell-nails",
"currency": "NZD",
"timezone": "Pacific/Auckland"
},
"services": [
{
"id": "svc_abc123",
"name": "Gel Manicure",
"description": "Long-lasting gel polish with cuticle care.",
"category": "Nails",
"durationMin": 60,
"priceCents": 7500,
"currency": "NZD"
}
]
}
// description is the service description, or null when none is set.Errors
| Status | When |
|---|---|
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
GET /staff
PublicGET /api/v1/{slug}/staffList bookable team members
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.
curl https://bookatu.com/api/v1/your-salon/staff
Response example
{
"org": { "name": "Parnell Nails", "slug": "parnell-nails", "timezone": "Pacific/Auckland" },
"staff": [
{ "id": "stf_abc", "name": "Anna", "title": "Senior Nail Tech", "bio": null }
]
}
// title and bio are null when not set.Errors
| Status | When |
|---|---|
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
GET /availability
PublicGET /api/v1/{slug}/availabilityOpen slots for a service
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.
curl "https://bookatu.com/api/v1/your-salon/availability?serviceId=svc_abc123&from=2026-06-10&to=2026-06-14"
Response example
{
"org": { "name": "Parnell Nails", "slug": "parnell-nails", "timezone": "Pacific/Auckland" },
"days": [
{
"date": "2026-06-10",
"open": true,
"slots": [
{ "startMin": 540, "startAt": "2026-06-09T21:00:00.000Z" }
]
},
{ "date": "2026-06-11", "open": false, "slots": [] }
]
}
// startMin is minutes from local midnight; startAt is the UTC instant to book.Parameters
| Field | Type | In | Required | Notes |
|---|---|---|---|---|
| serviceId | string | query | Yes | An active, online-bookable service id. |
| from | string | query | Yes | Start date, YYYY-MM-DD. |
| to | string | query | Yes | End date, YYYY-MM-DD. Must be on or after from, and at most 60 days later. |
| staffId | string | query | No | Restrict slots to one team member. |
Errors
| Status | When |
|---|---|
| 400 | serviceId is missing, from/to are not YYYY-MM-DD, from is later than to, or the range exceeds 60 days. |
| 404 | The {slug} is unknown, or the serviceId is not an active, online-bookable service in this org. |
| 500 | Unexpected server error. |
POST /book
PublicPOST /api/v1/{slug}/bookCreate a booking (online source)
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.
curl -X POST https://bookatu.com/api/v1/your-salon/book \
-H "Content-Type: application/json" \
-d '{
"serviceId": "svc_abc123",
"startAt": "2026-06-10T09:00:00Z",
"name": "Alex Smith",
"email": "alex@example.com",
"phone": "+64 21 555 0100"
}'Response example
{
"ok": true,
"ref": "VRD-7QK2M9",
"appointmentId": "apt_abc123",
"status": "confirmed",
"needsPayment": false
}
// When a deposit is required, status is "pending" and a payLink is returned:
{
"ok": true,
"ref": "VRD-7QK2M9",
"appointmentId": "apt_abc123",
"status": "pending",
"needsPayment": true,
"payLink": "https://bookatu.com/your-salon/book/success?apt=apt_abc123"
}Request fields
| Field | Type | In | Required | Notes |
|---|---|---|---|---|
| serviceId | string | body | Yes | An active, online-bookable service id from GET /services. |
| startAt | string | body | Yes | ISO 8601 date-time, e.g. a startAt value from GET /availability ("2026-06-10T09:00:00Z"). |
| name | string | body | Yes | Customer name, 2–80 characters (trimmed). |
| string | body | Yes | A valid email address. | |
| phone | string | body | Yes | 6–30 characters; digits, spaces and + ( ) - only. |
| staffId | string | body | No | Specific staff id. Omit to let the system assign a free team member. |
| notes | string | body | No | Free text, max 500 characters. |
Errors
| Status | When |
|---|---|
| 400 | The request body is not valid JSON. |
| 422 | 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. |
| 409 | The slot was just taken, or the service is unavailable / inactive. |
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
GET /customers
Requires API keyGET /api/v1/{slug}/customersList customers
Up to 1000 customers in this org (id, name, email, phone), ordered by name. Requires a valid API key whose org matches the {slug}.
curl https://bookatu.com/api/v1/your-salon/customers \ -H "Authorization: Bearer bk_live_YOUR_KEY"
Response example
{
"customers": [
{ "id": "cus_xyz", "name": "Alex Smith", "email": "alex@example.com", "phone": "+64211234567" }
]
}
// Capped at 1000 customers, ordered by name.Errors
| Status | When |
|---|---|
| 401 | The API key is missing or invalid. |
| 403 | The key belongs to a different organisation. |
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
GET /appointments
Requires API keyGET /api/v1/{slug}/appointmentsList upcoming appointments
Upcoming appointments with status pending or confirmed whose startAt is now or later, ordered by start time, capped at 200. Requires a valid API key scoped to this org.
curl https://bookatu.com/api/v1/your-salon/appointments \ -H "Authorization: Bearer bk_live_YOUR_KEY"
Response example
{
"appointments": [
{
"id": "apt_abc123",
"ref": "VRD-7QK2M9",
"serviceName": "Gel Manicure",
"startAt": "2026-06-10T21:00:00.000Z",
"endAt": "2026-06-10T22:00:00.000Z",
"durationMin": 60,
"priceCents": 7500,
"status": "confirmed",
"customerId": "cus_xyz",
"staffId": "stf_abc",
"notes": null,
"createdAt": "2026-06-01T03:12:00.000Z"
}
]
}
// Only pending + confirmed, startAt >= now, oldest first, max 200 rows.Errors
| Status | When |
|---|---|
| 401 | The API key is missing or invalid. |
| 403 | The key belongs to a different organisation. |
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
POST /appointments
Requires API keyPOST /api/v1/{slug}/appointmentsCreate a booking (admin source)
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.
curl -X POST https://bookatu.com/api/v1/your-salon/appointments \
-H "Authorization: Bearer bk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"serviceId": "svc_abc123",
"startAt": "2026-06-10T09:00:00Z",
"name": "Alex Smith",
"email": "alex@example.com",
"phone": "+64 21 555 0100"
}'Response example
{
"ok": true,
"ref": "VRD-7QK2M9",
"appointmentId": "apt_abc123",
"status": "confirmed",
"needsPayment": false
}
// When a deposit is required, status is "pending" and a payLink is returned:
{
"ok": true,
"ref": "VRD-7QK2M9",
"appointmentId": "apt_abc123",
"status": "pending",
"needsPayment": true,
"payLink": "https://bookatu.com/your-salon/book/success?apt=apt_abc123"
}Request fields
| Field | Type | In | Required | Notes |
|---|---|---|---|---|
| serviceId | string | body | Yes | An active, online-bookable service id from GET /services. |
| startAt | string | body | Yes | ISO 8601 date-time, e.g. a startAt value from GET /availability ("2026-06-10T09:00:00Z"). |
| name | string | body | Yes | Customer name, 2–80 characters (trimmed). |
| string | body | Yes | A valid email address. | |
| phone | string | body | Yes | 6–30 characters; digits, spaces and + ( ) - only. |
| staffId | string | body | No | Specific staff id. Omit to let the system assign a free team member. |
| notes | string | body | No | Free text, max 500 characters. |
Errors
| Status | When |
|---|---|
| 400 | The request body is not valid JSON. |
| 422 | 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. |
| 409 | The service is unavailable / inactive, or that exact staff member is already booked at that instant (database uniqueness guard). |
| 401 | The API key is missing or invalid. |
| 403 | The key belongs to a different organisation. |
| 404 | No business exists for the {slug}. |
| 500 | Unexpected server error. |
POST /appointments/{id}/cancel
Requires API keyPOST /api/v1/{slug}/appointments/{id}/cancelCancel a booking
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.
curl -X POST https://bookatu.com/api/v1/your-salon/appointments/apt_abc123/cancel \
-H "Authorization: Bearer bk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer requested" }'Response example
{ "ok": true, "id": "apt_abc123", "forfeited": false }Request fields
| Field | Type | In | Required | Notes |
|---|---|---|---|---|
| id | string | path | Yes | The appointment id to cancel. |
| reason | string | body | No | Optional cancellation reason, truncated to 200 characters and stored on the booking. |
Errors
| Status | When |
|---|---|
| 401 | The API key is missing or invalid. |
| 403 | The key belongs to a different organisation. |
| 404 | No business exists for the {slug}. |
| 400 | The booking could not be cancelled (e.g. the appointment id is unknown in this org). |
| 500 | Unexpected server error. |
Webhooks
SignedReceive a POST to your own https endpoint when bookings change. Add an endpoint in your admin dashboard at /{slug}/admin/api (admins only). Payloads carry no customer contact details. Fetch the full record from GET /appointments with your API key when you need more.
| Event | When it fires |
|---|---|
| booking.created | A booking is created (online, admin or via the API). |
| booking.cancelled | A booking is cancelled (by staff, the customer, or the API). |
| booking.completed | A booking is marked complete after the visit. |
| booking.rescheduled | A booking is moved to a new time (startAt is the new time). |
booking.created payload
{
"event": "booking.created",
"sentAt": "2026-06-01T03:12:00.000Z",
"data": {
"id": "apt_abc123",
"ref": "VRD-7QK2M9",
"service": "Gel Manicure",
"startAt": "2026-06-10T21:00:00.000Z",
"durationMin": 60,
"priceCents": 7500,
"status": "pending"
}
}booking.cancelled payload
{
"event": "booking.cancelled",
"sentAt": "2026-06-02T09:30:00.000Z",
"data": {
"id": "apt_abc123",
"ref": "VRD-7QK2M9",
"service": "Gel Manicure",
"startAt": "2026-06-10T21:00:00.000Z",
"status": "cancelled"
}
}booking.completed payload
{
"event": "booking.completed",
"sentAt": "2026-06-10T23:05:00.000Z",
"data": {
"id": "apt_abc123",
"ref": "VRD-7QK2M9",
"service": "Gel Manicure",
"startAt": "2026-06-10T21:00:00.000Z",
"status": "completed"
}
}booking.rescheduled payload
{
"event": "booking.rescheduled",
"sentAt": "2026-06-03T11:00:00.000Z",
"data": {
"id": "apt_abc123",
"ref": "VRD-7QK2M9",
"service": "Gel Manicure",
"startAt": "2026-06-12T21:00:00.000Z",
"status": "confirmed"
}
}Delivery & signing
Each delivery is signed with HMAC-SHA256 over the string "{timestamp}.{rawBody}", hex-encoded. Verify it before trusting the body. The endpoint secret is shown once when you create it and starts with whsec_.
X-Bookatu-EventThe event name, e.g. booking.created.X-Bookatu-Signaturet={unixSeconds},v1={hmacSha256Hex}. Use t when recomputing the signature.X-Bookatu-AttemptDelivery attempt number, 1 to 3.User-AgentBookatu-Webhooks/1Content-Typeapplication/json
- Up to 3 attempts. Backoff between attempts is 500ms, then 1000ms, then 2000ms (the formula is capped at 4000ms). A delivery succeeds on any 2xx response; non-2xx responses, timeouts and network errors are retried.
- Each attempt times out after 4 seconds.
- Redirects are not followed (redirect: error). Endpoints must be public https URLs: localhost, .local / .internal / .localhost hosts and private, reserved, loopback and cloud-metadata IP ranges are rejected (SSRF protection), both when the endpoint is saved and again at send time.
Verify every delivery. Recompute the signature over {timestamp}.{rawBody} and compare it, in a timing-safe way, to the v1 value in X-Bookatu-Signature.
import crypto from "node:crypto";
// rawBody is the exact bytes you received (do not re-serialize).
// secret is your endpoint secret (whsec_...).
// t comes from the X-Bookatu-Signature header: "t=<seconds>,v1=<hmac>".
function verify(rawBody, t, v1, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Rate limits
There is currently no per-key rate limit enforced on the v1 REST API. That is not a promise of unlimited throughput. Build clients defensively: treat HTTP 429 and 5xx responses as retryable, back off (ideally exponentially, with jitter), and cache rarely-changing responses such as business, services and staff. Limits may be introduced in future, designed not to break well-behaved clients.
MCP server
AI agentsBookatu exposes a Model Context Protocol (MCP) server so any AI assistant (Claude, GPT-4o, Gemini, or a custom agent) can list services, check real-time availability, create bookings, and look up customer and appointment data in a single authenticated session.
Endpoint
https://bookatu.com/api/mcp
Protocol
Streamable HTTP transport, JSON-RPC 2.0. Authenticate with the same bk_live_ key as the REST API (Authorization: Bearer bk_live_YOUR_KEY). The org is resolved from the key automatically — no org slug is needed in the URL.
# Discover tools
curl -X POST https://bookatu.com/api/mcp \
-H "Authorization: Bearer bk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'| Tool | Description |
|---|---|
| list_services | List all active bookable services. |
| get_availability | Get open slots for a service over a date range (max 60 days). |
| create_booking | Book an appointment for a customer; returns a ref and, when a deposit is due, status pending. |
| list_customers | List all customers in the org. |
| list_appointments | List upcoming pending and confirmed appointments. |
# Call a tool
curl -X POST https://bookatu.com/api/mcp \
-H "Authorization: Bearer bk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_availability",
"arguments": {
"serviceId": "svc_abc123",
"from": "2026-06-10",
"to": "2026-06-14"
}
}
}'Configure your AI assistant by pointing it at https://bookatu.com/api/mcpwith your API key. All tools are automatically scoped to the key's organisation.
Ready to build?
Create a free account and generate your first API key from the admin dashboard.