Bookings
Dana is every other Thursday at 9am. Recurring plans, the individual visits they generate, and checking a slot is actually free.
Dana has Brightside every other Thursday at 9am — two hours, two cleaners. That arrangement is one thing; the visit on 6 August is another. When Dana goes on holiday, you move that Thursday without touching the arrangement.
That's the shape of this module, and the one idea worth getting straight before you write any code:
- A schedule plan is the arrangement. "Every other Thursday at 9, ongoing."
- A schedule instance is one actual visit. "Thursday 6 August, 9–11am, Marcus and Priya."
Plans generate instances. You book, move, cancel and invoice instances.
Read it instead
Bookings keeps the arrangement and the visit apart on purpose. Dana every other Thursday at nine is one thing; the visit on the sixth of August is another. Both over REST.
A plan carries a client_id, a type of recurring or one_time, a recurrence_interval from daily to monthly, start and end dates, and the services it books. Each plan generates instances, which are the visits you can actually move.
Check availability before you promise a slot. Move one Thursday without touching the arrangement when Dana goes away. Turn a finished visit into an invoice, and subscribe to schedules.instance.rescheduled to keep your own calendar in step.
schedules.view reads plans, visits, availability and history. A read-only key can render a calendar without ever being able to move somebody's Thursday.
Bookings: recurring plans, the visits they generate, and an availability check before you promise anyone a time. The guide is below.
Before you start
- The module switched on for the site. If it isn't, everything below returns
403regardless of your key — check the site's modules on the Locations page first. - A credential with the right scopes —
schedules.viewto read,schedules.createto book. - A customer to book for, from the Customers module.
- Services to book, from the Services module.
What a plan looks like
Prop
Type
Times are HH:MM and nothing else
start_time and end_time are validated as exactly HH:MM in 24-hour form. "9:00"
fails, "09:00:00" fails, "9am" fails. Format them before sending, because the error
message won't be as clear as this paragraph.
Setting price on a service line lets you override what this customer pays without touching
the price list — useful for a long-standing rate you don't want to lose.
The endpoints
Check before you book
This is the part people skip and then regret. Ask whether a slot works before creating anything — it accounts for staff availability and capacity, which you can't reasonably work out yourself.
const check = await fetch(
'https://api.hyphabase.com/api/modules/schedules/check-availability',
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': readCookie('XSRF-TOKEN'),
'X-Location-Id': locationId,
},
body: JSON.stringify({
client_id: clientId,
schedule_plan_id: planId,
scheduled_date: '2026-08-06',
start_time: '09:00',
end_time: '11:00',
employee_ids: [marcusId, priyaId],
}),
},
).then((r) => r.json());
if (!check.available) {
// Offer something that is.
const { alternatives } = await findAlternatives();
return showAlternatives(alternatives);
}Offering alternatives beats reporting a clash. find-alternatives takes the same
shape plus duration, and hands back slots that would work.
Booking a visit
curl -X POST https://api.hyphabase.com/api/modules/schedules/schedule-instances \
-u sk_live_xxx: \
-H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: booking-8891" \
-d '{
"schedule_plan_id": "'"$PLAN_ID"'",
"client_id": "'"$CLIENT_ID"'",
"location_id": "'"$HYPHABASE_LOCATION_ID"'",
"scheduled_date": "2026-08-06",
"start_time": "09:00",
"end_time": "11:00",
"employee_ids": ["'"$MARCUS_ID"'", "'"$PRIYA_ID"'"],
"notes": "Key safe by the side gate."
}'Always send an Idempotency-Key. A customer tapping Book twice on a slow connection is
the exact scenario Safe retries exists for, and a
double booking is a crew turning up to an empty house.
force_override: true books anyway despite a clash. Reach for it when a human has decided
to double up deliberately — not as a way around a validation error you don't understand.
Moving, cancelling, skipping
Three different things, and picking the wrong one loses information:
| You want to | Use | It means |
|---|---|---|
| Change when a visit happens | reschedule | Same visit, new time. History kept. |
| Call off a visit | cancel | It won't happen. The arrangement continues. |
| Miss one occurrence | skip | Dana's on holiday this Thursday, back the next. |
Deleting an instance is also possible and is soft — restore brings it back, force
removes it permanently. Prefer cancel for anything a customer did, because it's a business
fact rather than a data-entry mistake.
Turning visits into invoices
This module reaches into billing, which is unusual and useful: you can invoice straight from what was scheduled instead of retyping it.
| For | Endpoint |
|---|---|
| Invoice one visit | POST /api/modules/schedules/schedule-instances/{scheduleInstance}/invoices |
| Invoice several at once | POST /api/modules/schedules/schedule-plans/{schedulePlan}/invoices/from-instances |
| See it before committing | POST /api/modules/schedules/schedule-plans/{schedulePlan}/invoices/preview |
| What's been billed | GET /api/modules/schedules/schedule-plans/{schedulePlan}/billing-status |
| Final balance | POST /api/modules/schedules/schedule-instances/{scheduleInstance}/invoices/final-payment |
invoice_type is required and is one of full, deposit, partial, final or addon.
With deposit or partial you also send percentage.
Two things worth knowing. preview charges nothing — same inputs, same output, no
invoice created, which is what you show a customer before they agree. And
only_unbilled: true on a bulk invoice skips visits already invoiced, which is what
makes a monthly billing run safe to re-run after it half-fails.
Reminders
GET /api/modules/schedules/notification-preferencesholds one setting for the whole site: whether email and SMS are on, and how far ahead to remind.
{
"email_enabled": true,
"sms_enabled": false,
"reminder_times": [1440, 120]
}reminder_times is minutes before the visit — so 1440 is the day before and 120 is two
hours ahead. All three fields are required when you save it.
shows what actually went out, which is the first place to look when a customer says they weren't told.
What you get told about
| Event | When |
|---|---|
schedules.instance.created | A visit is booked |
schedules.instance.cancelled | A visit is called off |
schedules.instance.rescheduled | A visit moves |
Note these are instance, not schedule_instance — the events are about visits, because
that's what changes. Nothing fires when a plan is created, so if you mirror arrangements you
need to poll or drive it from your own booking flow.
What to grant
| Scope | Lets you |
|---|---|
schedules.view | Read plans, visits, availability and history |
schedules.create | Book visits and create arrangements |
schedules.update | Move, cancel, skip, edit |
schedules.delete | Delete plans and visits |
schedules.view is safe for a publishable key, which is how a booking page shows what's
free without a server in between. Booking itself needs a secret key or a signed-in person.
What goes wrong
Recipes
A booking page that doesn't double-book. Call check-availability on slot selection,
find-alternatives when it comes back unavailable, and POST the instance with an
Idempotency-Key derived from your own booking id.
Thursday's rounds, per crew. List instances filtered by date and read
users/{userId}/availability for each cleaner. That's the basis of the 6am email in
Server apps.
Monthly billing. For each active plan, preview the invoice from unbilled instances,
then from-instances with only_unbilled: true. Check billing-status afterwards to
confirm nothing was missed.
Tell customers before they forget. Set reminder_times to [1440, 120] and check
notification-history when someone claims they weren't told.