Developer docs

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.

EndpointAuthDescription
GET /businessNoneBusiness profile + opening hours
GET /servicesNoneList active, online-bookable services
GET /staffNoneList bookable team members
GET /availabilityNoneOpen slots for a service
POST /bookNoneCreate a booking (online source)
GET /customersAPI keyList customers
GET /appointmentsAPI keyList upcoming appointments
POST /appointmentsAPI keyCreate a booking (admin source)
POST /appointments/{id}/cancelAPI keyCancel 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

Public
GET /api/v1/{slug}/business

Business 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
StatusWhen
404No business exists for the {slug}.
500Unexpected server error (details are never leaked).

GET /services

Public
GET /api/v1/{slug}/services

List 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
StatusWhen
404No business exists for the {slug}.
500Unexpected server error.

GET /staff

Public
GET /api/v1/{slug}/staff

List 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
StatusWhen
404No business exists for the {slug}.
500Unexpected server error.

GET /availability

Public
GET /api/v1/{slug}/availability

Open 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
FieldTypeInRequiredNotes
serviceIdstringqueryYesAn active, online-bookable service id.
fromstringqueryYesStart date, YYYY-MM-DD.
tostringqueryYesEnd date, YYYY-MM-DD. Must be on or after from, and at most 60 days later.
staffIdstringqueryNoRestrict slots to one team member.
Errors
StatusWhen
400serviceId is missing, from/to are not YYYY-MM-DD, from is later than to, or the range exceeds 60 days.
404The {slug} is unknown, or the serviceId is not an active, online-bookable service in this org.
500Unexpected server error.

POST /book

Public
POST /api/v1/{slug}/book

Create 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
FieldTypeInRequiredNotes
serviceIdstringbodyYesAn active, online-bookable service id from GET /services.
startAtstringbodyYesISO 8601 date-time, e.g. a startAt value from GET /availability ("2026-06-10T09:00:00Z").
namestringbodyYesCustomer name, 2–80 characters (trimmed).
emailstringbodyYesA valid email address.
phonestringbodyYes6–30 characters; digits, spaces and + ( ) - only.
staffIdstringbodyNoSpecific staff id. Omit to let the system assign a free team member.
notesstringbodyNoFree text, max 500 characters.
Errors
StatusWhen
400The request body is not valid JSON.
422Validation 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.
409The slot was just taken, or the service is unavailable / inactive.
404No business exists for the {slug}.
500Unexpected server error.

GET /customers

Requires API key
GET /api/v1/{slug}/customers

List 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
StatusWhen
401The API key is missing or invalid.
403The key belongs to a different organisation.
404No business exists for the {slug}.
500Unexpected server error.

GET /appointments

Requires API key
GET /api/v1/{slug}/appointments

List 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
StatusWhen
401The API key is missing or invalid.
403The key belongs to a different organisation.
404No business exists for the {slug}.
500Unexpected server error.

POST /appointments

Requires API key
POST /api/v1/{slug}/appointments

Create 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
FieldTypeInRequiredNotes
serviceIdstringbodyYesAn active, online-bookable service id from GET /services.
startAtstringbodyYesISO 8601 date-time, e.g. a startAt value from GET /availability ("2026-06-10T09:00:00Z").
namestringbodyYesCustomer name, 2–80 characters (trimmed).
emailstringbodyYesA valid email address.
phonestringbodyYes6–30 characters; digits, spaces and + ( ) - only.
staffIdstringbodyNoSpecific staff id. Omit to let the system assign a free team member.
notesstringbodyNoFree text, max 500 characters.
Errors
StatusWhen
400The request body is not valid JSON.
422Validation 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.
409The service is unavailable / inactive, or that exact staff member is already booked at that instant (database uniqueness guard).
401The API key is missing or invalid.
403The key belongs to a different organisation.
404No business exists for the {slug}.
500Unexpected server error.

POST /appointments/{id}/cancel

Requires API key
POST /api/v1/{slug}/appointments/{id}/cancel

Cancel 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
FieldTypeInRequiredNotes
idstringpathYesThe appointment id to cancel.
reasonstringbodyNoOptional cancellation reason, truncated to 200 characters and stored on the booking.
Errors
StatusWhen
401The API key is missing or invalid.
403The key belongs to a different organisation.
404No business exists for the {slug}.
400The booking could not be cancelled (e.g. the appointment id is unknown in this org).
500Unexpected server error.

Webhooks

Signed

Receive 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.

EventWhen it fires
booking.createdA booking is created (online, admin or via the API).
booking.cancelledA booking is cancelled (by staff, the customer, or the API).
booking.completedA booking is marked complete after the visit.
booking.rescheduledA 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/1
  • Content-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 agents

Bookatu 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":{}}'
ToolDescription
list_servicesList all active bookable services.
get_availabilityGet open slots for a service over a date range (max 60 days).
create_bookingBook an appointment for a customer; returns a ref and, when a deposit is due, status pending.
list_customersList all customers in the org.
list_appointmentsList 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.