Guides
Bookings
Your own booking page for visitors who are not signed in, and staff screens for the business's own team. List what can be booked and when, book a time, and let customers cancel or move from the link in their email.
Customers book themselves on the business's own website: they pick a service and a time, give their name and email, and get an email with a calendar invite and a link to change or cancel. The business gets a notice of each booking, and every booking lands in Contacts, linked to the person who made it.
The quickest way is the booking box: paste two lines from Contacts, Bookings, On your website into a page, and it does all of this. Use createBookingsClient() when you want the page to look and work your own way.
Bookings is reaching accounts in stages. If it isn't on the account yet, a call answers with an
AwesomateErrorwhoseserverCodeisfeature_unavailable, and the hub says so too.
import { createBookingsClient } from '@awesomate/sdk';
const bookings = createBookingsClient({ key: 'bk_your_booking_key' });The booking key is public: it sits in the page. It works only on the websites listed for it, opens only the business's bookings, and works on every plan. No one signs in.
What can be booked, and when
const { business, services } = await bookings.services();
const service = services[0];
const times = await bookings.openTimes(service.key, { from: new Date(), to: new Date(Date.now() + 14 * 86_400_000) });
for (const t of times) {
// UTC times: show them in the calendar's own zone
const zone = service.calendars.find((c) => c.key === t.calendar)?.timezone;
render(new Date(t.start).toLocaleString('en-AU', { timeZone: zone }), t.calendarName, t.seatsLeft);
}- Times are UTC. Show them in the calendar's own zone (
service.calendars[].timezone), and say which zone it is, so nobody books an hour out across a daylight-saving change. - A class (
capacityabove 1) is one start several people join.seatsLeftsays how many places are left, andjoinsis true when someone has already booked that start. - At most 62 days come back at once. Ask for the next window to go further.
Booking
const attempt = crypto.randomUUID(); // one per booking, kept across retries
try {
const booked = await bookings.book({
service: service.key,
calendar: time.calendar,
startsAt: time.start,
name: 'Pat Lee',
email: 'pat@example.com',
answers: { reason: 'Back pain' },
idempotencyKey: attempt,
});
showMessage('You are booked in. We have emailed you the details.');
render(booked.manageUrl);
} catch (err) {
if (err instanceof AwesomateError && err.code === 'conflict') {
showMessage(err.personMessage ?? 'That time was just taken. Please choose another.');
} else throw err;
}- Ask the service's questions (
service.intake). A question markedrequiredmust be answered, andselectandmultiselectanswers must be one of itsoptions. - Pass an
idempotencyKeymade once per booking: a retry after a dropped connection then returns the same booking (created: false) instead of a second one. - A time can go between listing it and booking it. The hub checks again under a lock and answers
conflictwithfieldsaying why:not_open,slot_taken,too_many_seats,session_fullorday_full. List the times again. - Limits are for people, not code. One email holds at most three bookings coming up (
field: 'too_many_open'), each address may book ten times in fifteen minutes, and Essentials takes 50 website bookings a month (field: 'monthly_limit'). ShowpersonMessage.
The customer's own link
Every booking email carries a link to the hub's page for that booking, the same as booked.manageUrl. To handle it on your own site instead, read the token from the link:
const token = manageTokenFrom('https://hub.awesomate.ai/booking?t=abc');
if (token) {
const { booking, look } = await bookings.booking(token);
// look.logo, look.colour and look.website, each null when the business has not set it
if (booking.canMove) {
const times = await bookings.openTimesToMove(token);
await bookings.move(token, times[0].start);
}
if (booking.canCancel) await bookings.cancel(token, 'Feeling better');
}The link is the customer's credential for this one booking: it needs no booking key and names no person. Changes close when the service says (cancel_cutoff_hours, 24 hours before by default). After that, canCancel and canMove are false, cancel() and move() answer conflict with field: 'too_late', and the customer should contact the business.
Staff screens
The business's own team works with every booking from your server, with the account's token. Use it for a screen at the front desk, a daily run sheet, or an n8n workflow that books a time when a job is won. It isn't for visitors: the token is a secret, so keep it on a server.
import { createClient } from '@awesomate/sdk';
const db = createClient({ token: process.env.AWESOMATE_TOKEN! });
// The services and calendars: their keys are what openTimes() and book() take
const setup = await db.bookings.setup();
for (const s of setup.services) console.log(s.key, s.name, 'on', s.calendars.join(', '));
// Tomorrow's run sheet, with each customer
const day = await db.bookings.list({ from: '2026-10-08T00:00:00+11:00', to: '2026-10-09T00:00:00+11:00', status: 'confirmed' });
for (const b of day) console.log(b.startsAt, b.serviceName, b.customer?.name, b.customer?.phone);
// Book a caller in, outside the usual hours, and confirm it on the phone instead of by email
const times = await db.bookings.openTimes('consult', { calendar: 'jo' });
const booked = await db.bookings.book({
service: 'consult', calendar: 'jo', startsAt: times[0].start,
email: 'sam@example.com', firstName: 'Sam', phone: '0400 000 000',
idempotencyKey: crypto.randomUUID(), notify: false,
});
await db.bookings.move(booked.bookingId, '2026-10-09T10:00:00+11:00', { calendar: 'alex' });
await db.bookings.outcome(booked.bookingId, 'completed');| Method | Does |
|---|---|
setup() |
Whether bookings are on, the calendars with their hours, the services with the calendars that take them, and this month's online bookings against the plan's. |
list({ from, to, status, calendar, limit }) |
Bookings starting in a window (a day ago for 31 days unless told), each with its customer's name, email and phone. |
get(id) |
One booking, or null. |
openTimes(service, { from, to, calendar, seats }) |
The same open times customers see. |
book({ ... }) |
Books for a customer named by email, found in Contacts or added. outsideHours: true skips the calendar's hours and notice; overlap, seats and the daily limit still apply. |
cancel(id, { reason, notify }) |
Cancels it. |
move(id, startsAt, { calendar, outsideHours, notify }) |
Moves it, on the same calendar or another. |
outcome(id, 'completed' | 'no_show') |
Records how it went. Sends no email. |
- Reading works on every plan (
crm:read). Booking, cancelling, moving and outcomes needcrm:write, which tokens carry on Support Plus and above. Bookings must be on the account: a call answersfeature_unavailablewhen it isn't. - The customer and the business hear about it. Booking, moving and cancelling email the customer and the calendar's notice address. Pass
notify: falseto skip the customer's email when you have told them yourself. - A time that isn't free answers
conflictwith the samefieldreasons asbook()above. A booking that isn't confirmed, or a time that has passed, answersvalidation. - Staff limits are not the website's: there is no three-booking or monthly limit here.
- An app's server key (
ak_...) can't usedb.bookings.
What it does not do yet
- No payments or deposits.
price_textis shown, never charged. - Outlook is not connected yet. A calendar connected to Google in the hub already keeps busy times out of
openTimes()and gets every booking written to it; nothing in the SDK changes for that.