Skip to content
Keptslot
Start free

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.

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.

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.

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.

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.

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.

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

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.