HyphaBase

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.

What you can build with Bookings · 1:15
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 403 regardless of your key — check the site's modules on the Locations page first.
  • A credential with the right scopes — schedules.view to read, schedules.create to 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 toUseIt means
Change when a visit happensrescheduleSame visit, new time. History kept.
Call off a visitcancelIt won't happen. The arrangement continues.
Miss one occurrenceskipDana'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.

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-preferences

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

GET /api/modules/schedules/notification-history

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

EventWhen
schedules.instance.createdA visit is booked
schedules.instance.cancelledA visit is called off
schedules.instance.rescheduledA 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

ScopeLets you
schedules.viewRead plans, visits, availability and history
schedules.createBook visits and create arrangements
schedules.updateMove, cancel, skip, edit
schedules.deleteDelete 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.

Next

On this page