Availability and booking, end to end
There are two ways to reach the same booking, and which one you are building decides almost everything else.
The two paths
Section titled “The two paths”From your server, with a secret key, you read availability and create the booking directly. Nothing else is involved. This is the shape of an integration: a CRM that books on a customer’s behalf, an internal tool, a migration script.
From a browser, with the publishable key, you are building something customer-facing. The customer’s browser bootstraps itself from the workspace slug, gets the publishable key in that response, and then makes the same availability and create calls the server would.
The endpoints are identical. Only the credential and the origin differ.
The browser path starts with a slug
Section titled “The browser path starts with a slug”GET /v1/public/workspaces/{slug} takes no key. It returns, in one response,
everything a booking page needs to render its first screen: the workspace and
its timezone, the bookable services, the calendars, the custom questions the
form asks, and the workspace’s publishable key.
That one call is why a booking page can be a static page. There is nothing to configure and no server to run.
One field on that response deserves a warning. require_verification tells you
that a 6-digit code is likely to follow, and it is useful for warning the
customer in advance. It is not how you decide what screen comes next. A
business can change the setting between the bootstrap call and the booking. The
status on the create response is the authority; see below.
Reading availability
Section titled “Reading availability”GET /v1/availability takes a service, a from and a to. The range must be
62 days or less.
The response groups slots by calendar, because “who is free” is a real question
for a business with more than one chair. If your interface does not let the
customer pick a person, flatten the groups and pick one for them — or omit
staff_id on the create call and let the API choose.
Two things about availability are easy to get wrong:
Every timestamp is in the workspace’s timezone, not the caller’s and not UTC. The appointment happens at the business. A customer in another country sees their 9am, and that is a display concern for your client, not a thing to convert on the way in.
Availability is advisory, not a reservation. It reflects the moment you
asked. Two customers looking at the same screen see the same slot, and the
first one to POST gets it. The second gets a 409 with slot_unavailable,
and that is a normal outcome to design for, not an error state.
A pending hold does count as busy while it lives, so it does not show up as
an open slot. The gap that produces a 409 is simply the time between your
read and your write.
Creating the booking
Section titled “Creating the booking”POST /v1/bookings takes a service, a start time and a customer. staff_id is
optional — omit it and the API picks a calendar that is free.
Send an Idempotency-Key. A booking is the one thing in this API you must not
accidentally do twice, and a timed-out request that actually succeeded is the
ordinary way that happens. See the rate limits and quotas
guide.
The response carries two fields that exist nowhere else.
access_token is the customer’s manage link. It appears on this response
and on no later one. If you do not store it here, the customer’s cancel and
reschedule links are gone — they still exist in the email we send, but you
cannot rebuild them.
status is confirmed or pending, and it is how you decide what happens
next. Not require_verification, not a setting you cached at bootstrap: this
field, on this response.
The pending hold
Section titled “The pending hold”A pending booking occupies the slot. The customer has been emailed a 6-digit
code and has not entered it yet.
Send the code to POST /v1/bookings/{id}/verify. On success the booking becomes
confirmed.
Every failure of that call returns the same error — verification_failed — for
a wrong code, an expired hold, too many attempts, and a booking id that does not
exist. They are collapsed on purpose. Six digits is a small space, and a
response that distinguished “wrong code” from “no such booking” would be an
oracle for guessing it.
A hold that nobody verifies lapses on its own and frees the slot. Your client does not have to clean up after an abandoned form.
Note that confirmation, not creation, is what counts against the plan
allowance. That is why verify can answer 402.
Moving a booking that already exists
Section titled “Moving a booking that already exists”POST /v1/bookings/{id}/reschedule moves a booking to a new time.
To show the customer what they can move to, call availability with
exclude_booking_id set to the booking being moved. Without it, the booking’s
own slot reads as busy, and the one time the customer is most likely to want —
half an hour later, same day — is missing from the list for no visible reason.
Send exclude_booking_id with a value or leave it out entirely. An empty value
is refused with a 422; empty and absent are different things here.
The customer’s own view
Section titled “The customer’s own view”The token in access_token addresses /v1/public/bookings/{token}, which is
the manage link. It returns the booking, the business, and two booleans:
can_cancel and can_reschedule.
Use those to enable buttons. Each one is true exactly when that call would succeed, and they are not copies of each other — a pending booking inside the notice window can be cancelled but not moved.
An unknown token, an expired token and the token of an already-cancelled
booking all return the same 404. Cancelling clears the token, so a cancelled
booking resolves to no row, exactly like a token that never existed.
What you hear about afterwards
Section titled “What you hear about afterwards”If your server needs to know about bookings it did not make — the customer
cancelled from their email, an operator moved it in the dashboard — that is
what webhooks are for. Polling GET /v1/bookings
works and will not scale gracefully.