Awesomate docs v0.27.0

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 AwesomateError whose serverCode is feature_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 (capacity above 1) is one start several people join. seatsLeft says how many places are left, and joins is 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 marked required must be answered, and select and multiselect answers must be one of its options.
  • Pass an idempotencyKey made 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 conflict with field saying why: not_open, slot_taken, too_many_seats, session_full or day_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'). Show personMessage.

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 need crm:write, which tokens carry on Support Plus and above. Bookings must be on the account: a call answers feature_unavailable when 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: false to skip the customer's email when you have told them yourself.
  • A time that isn't free answers conflict with the same field reasons as book() above. A booking that isn't confirmed, or a time that has passed, answers validation.
  • Staff limits are not the website's: there is no three-booking or monthly limit here.
  • An app's server key (ak_...) can't use db.bookings.

What it does not do yet

  • No payments or deposits. price_text is 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.