GlowBook API v1
The public REST API lets a WhatsApp assistant, a website widget or any internal tool read a salon's catalogue and availability and create, move and cancel bookings. Everything is scoped to the salon (tenant) that owns the API key. This page is served live at /api/v1/docs.
Basics
- Base URL:
https://<your-glowbook-host>/api/v1(local dev:http://localhost:3000/api/v1). - Authentication:
Authorization: Bearer gbk_live_…. Create keys in Settings → Integrations → API keys. The key is shown once; GlowBook stores only its SHA-256 hash. - Scopes:
read(GET endpoints) andwrite(POST endpoints,includeHidden,force). New keys get both. - Rate limit: 60 requests per minute per key. Responses carry
x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset(epoch seconds); over the limit you get429 {"error":"rate_limited"}. - Format: JSON in and out. Timestamps are ISO-8601 UTC (
2026-09-10T03:30:00.000Z). Dates and times you send are local to the salon (date=YYYY-MM-DD,time=HH:MM24h, salon timezone fromGET /me). Money is integer cents in the salon currency. - Errors:
{"error": "<code>", "message": "<human text>"}with status401unauthorized,403forbidden (scope),404not_found,409slot_taken,422invalid / policy / too_late / too_early / closed / staff_unavailable,429rate_limited.
All examples below use the demo salon Pageant Lashes (pageant-lashes). Replace $KEY with your key and $BASE with your base URL.
export BASE=http://localhost:3000/api/v1
export KEY=gbk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GET /me
Public info about the salon plus the booking rules the API enforces.
curl -s $BASE/me -H "Authorization: Bearer $KEY"
{
"id": "…", "name": "Pageant Lashes by Serenity Beaute", "slug": "pageant-lashes",
"timezone": "Asia/Singapore", "currency": "SGD", "locale": "en-SG",
"phone": "+65 8938 2624", "whatsapp": "6589382624", "address": "10 Anson Road #02-13, …",
"bookingUrl": "http://localhost:3000/book/pageant-lashes",
"bookingSettings": { "minLeadMinutes": 120, "maxAdvanceDays": 60, "slotStepMin": 15, "cancelPolicyHours": 24, "confirmMode": "auto", "requirePhone": true, "requireEmail": false, "cancellationPolicyText": "…" },
"apiKey": { "name": "SmartFAQ", "prefix": "gbk_live_abcd", "scopes": ["read", "write"] }
}
GET /services
Categories with their services, add-ons and the staff who can perform each one. Services with visibility: "hidden" are omitted unless ?includeHidden=1 (needs write). visibility: "members" services are returned so a bot can explain them, but online booking rules still apply.
curl -s "$BASE/services" -H "Authorization: Bearer $KEY"
{
"currency": "SGD",
"categories": [
{ "id": "…", "name": "Lashes — First Trial", "visibility": "public",
"services": [
{ "id": "svc_1", "name": "Volume 5-7D Glorious Lashes", "durationMin": 90, "bufferAfterMin": 10,
"priceCents": 10800, "compareAtCents": 14800, "depositCents": 0, "firstVisitOnly": true,
"visibility": "public", "bookableOnline": true, "rebookIntervalDays": 21,
"addons": [ { "id": "add_1", "name": "Lower Lash Extension", "durationMin": 20, "priceCents": 2800 } ],
"staffIds": ["stf_L", "stf_S"] }
] }
],
"addons": [ … ]
}
GET /staff
curl -s $BASE/staff -H "Authorization: Bearer $KEY"
Returns { "staff": [ { "id", "name", "title", "skills": [], "bookableOnline": true, "workingDays": ["mon","tue",…] } ] }.
GET /availability
Free start times for one or more services on a date. Multi-service chains are booked back-to-back with one staff member. Uses the public rules (lead time, advance window, online-bookable staff only).
| Query | Required | Notes |
|---|---|---|
date | yes | YYYY-MM-DD in the salon timezone |
service | yes | one id, or several comma-separated for a chain |
addons | no | comma-separated add-on ids (extend duration/price) |
staff | no | restrict to one staff id |
curl -s "$BASE/availability?date=2026-09-10&service=svc_1" -H "Authorization: Bearer $KEY"
{
"date": "2026-09-10", "timezone": "Asia/Singapore", "durationMin": 90, "priceCents": 10800, "depositCents": 0, "currency": "SGD",
"reason": null,
"staff": [ { "id": "stf_L", "name": "Beautician L" }, { "id": "stf_S", "name": "Beautician S" } ],
"slots": [ { "time": "11:00", "staffIds": ["stf_L", "stf_S"] }, { "time": "11:15", "staffIds": ["stf_L"] } ]
}
reason is set when there are no slots for a structural reason: past, too_far, too_late (inside lead time), not_bookable, no_staff.
GET /availability/days
Which dates in a window have at least one slot — handy for "what's the next day you have?".
curl -s "$BASE/availability/days?from=2026-09-10&days=14&service=svc_1" -H "Authorization: Bearer $KEY"
# → { "from": "2026-09-10", "days": 14, "available": ["2026-09-10", "2026-09-11", …] }
POST /bookings
Create a booking (201). Client is matched by phone (preferred) or email, otherwise created. Requires write.
curl -s -X POST $BASE/bookings -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{
"date": "2026-09-10", "time": "15:00",
"serviceIds": ["svc_1"], "addonIds": [], "staffId": null,
"client": { "firstName": "Aisha", "lastName": "Rahman", "phone": "+65 9123 4567", "email": "aisha@example.com", "waId": "6591234567" },
"notes": "Prefers a natural cat-eye map",
"couponCode": "WELCOME10",
"source": "whatsapp",
"externalRef": "wa:6591234567:msg_123"
}'
Body fields: date, time, serviceIds[] (required); addonIds[], staffId (null/omitted = any available, load-balanced), client ({id} for an existing client, or {firstName, lastName?, phone, email?, waId?}), notes, couponCode, source (whatsapp | api | ai, default api), externalRef.
{
"bookings": [ { "id": "bk_1", "code": "GB-7K3M2", "serviceName": "Volume 5-7D Glorious Lashes", "startsAt": "2026-09-10T07:00:00.000Z", "endsAt": "2026-09-10T08:30:00.000Z", "status": "confirmed", "source": "whatsapp", "priceCents": 10800, "discountCents": 1080, "riskScore": 25, "manageUrl": "http://localhost:3000/b/…", "staffId": "stf_L", "clientId": "cl_1" } ],
"client": { "id": "cl_1", "firstName": "Aisha", "lastName": "Rahman", "phone": "6591234567", "visitCount": 0 },
"manageUrl": "http://localhost:3000/b/…"
}
Errors: 409 slot_taken (pick another slot — re-query availability), 422 too_late (inside lead time), 422 policy (first-visit-only promo for a returning client, coupon invalid, blocked client), 422 invalid (missing phone when the salon requires it), 404 not_found (service/client id).
GET /bookings
List bookings in a local date range (inclusive, default today → +7 days, max 200).
| Query | Notes |
|---|---|
from, to | YYYY-MM-DD |
status | pending confirmed arrived in_progress completed no_show cancelled |
clientId | filter by client |
phone | filter by client phone (any format; normalised) — the easy way to answer "what's my booking?" |
curl -s "$BASE/bookings?phone=6591234567&from=2026-09-01&to=2026-10-31" -H "Authorization: Bearer $KEY"
Each item is a booking object plus staffName and client.
GET /bookings/:id
curl -s $BASE/bookings/bk_1 -H "Authorization: Bearer $KEY"
# → { "booking": {…}, "group": [ … all services booked together … ], "client": {…}, "staff": { "id", "name" } }
POST /bookings/:id/cancel
Cancels the booking and every service booked with it. The salon's cancellation window (cancelPolicyHours) is enforced and returns 422 policy when too close — pass ?force=1 (needs write) to override, e.g. when the salon itself cancels.
curl -s -X POST "$BASE/bookings/bk_1/cancel" -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{"reason":"Client unwell"}'
curl -s -X POST "$BASE/bookings/bk_1/cancel?force=1" -H "Authorization: Bearer $KEY"
Returns { "bookings": [ …cancelled… ] }. The client is notified through the outbox and the waitlist is offered the freed slot automatically.
POST /bookings/:id/reschedule
curl -s -X POST "$BASE/bookings/bk_1/reschedule" -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{"date":"2026-09-12","time":"16:00"}'
Body: date, time (required), staffId (omit to keep the same staff member; null to allow any). 409 slot_taken when the new time isn't free; 422 policy when inside the cancellation window unless ?force=1. Requires write.
POST /bookings/:id/status
Front-desk status changes (requires write).
curl -s -X POST "$BASE/bookings/bk_1/status" -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{"status":"completed","paidCents":10800,"paymentMethod":"paynow"}'
status: confirmed (approve a pending request) arrived in_progress completed no_show cancelled. With completed you may pass paidCents, paymentMethod, tipCents.
GET /clients
Search. At least one of phone, email, q (name / email / phone fragment). Max 50.
curl -s "$BASE/clients?phone=%2B6591234567" -H "Authorization: Bearer $KEY"
Client objects expose id, firstName, lastName, phone, email, tags, visitCount, noShowCount, lastVisitAt, loyaltyPoints.
POST /clients
Find-or-create by phone/email. 201 when created, 200 when an existing client matched. Requires write.
curl -s -X POST $BASE/clients -H "Authorization: Bearer $KEY" -H "content-type: application/json" -d '{"firstName":"Aisha","phone":"+65 9123 4567","waId":"6591234567"}'
GET /clients/:id
Client card summary: contact + stats, upcoming bookings, recent completed bookings and pinnedNotes (kinds: allergy, formula, preference, note) so a bot can warn about sensitivities before booking.
curl -s $BASE/clients/cl_1 -H "Authorization: Bearer $KEY"
Webhooks
Configure endpoints in Settings → Integrations → Webhooks. Each delivery is a POST with JSON { "event", "at", "tenantId", "data" } and headers:
X-GlowBook-Event: booking.createdX-GlowBook-Signature: sha256=<hex HMAC-SHA256 of the raw request body using the webhook secret>
Events: booking.created, booking.updated (data.change = rescheduled | new status), booking.cancelled, client.created, review.created, waitlist.offered. Deliveries retry with backoff (up to 6 attempts) until your endpoint returns 2xx. Verify the signature before trusting the payload:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
Booking object
| Field | Type | Notes |
|---|---|---|
id, code | string | code is the short human reference (GB-XXXXX) |
groupId | string · null | shared by services booked together |
clientId, staffId, serviceId | string · null | |
serviceName | string | denormalised for history |
startsAt, endsAt, createdAt | ISO UTC | |
durationMin | int | |
status | enum | pending confirmed arrived in_progress completed no_show cancelled |
source | enum | online admin api whatsapp walk_in phone ai |
priceCents, discountCents, depositCents, paidCents | int | cents |
paymentStatus | enum | unpaid deposit paid refunded waived |
addons | array | {id, name, priceCents, durationMin} |
clientNotes | string · null | what the client typed at booking |
riskScore | int · null | 0-100 no-show risk |
manageUrl | string | client self-service link (view / reschedule / cancel / review) |
Changelog
- v1 (2026-09): initial release — services, staff, availability, bookings, clients, docs, signed webhooks.