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 a publishable key, you are building something customer-facing: a booking form on a website. Build a booking page covers that case from setup to deposit, and this page describes the calls both paths share.

The endpoints are identical. Only the credential and the origin differ. One thing is not the same: a booking made with a secret key never takes a deposit, whatever the service charges. It is confirmed straight away.

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, and the custom questions the form asks.

It also returns the workspace’s publishable_key. It works from your site once your site’s origin is on that key’s allowed list; see the keys guide.

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.

The response also carries online_bookings_paused. While it is true, the workspace is not taking online bookings: availability, creating a booking and rescheduling answer 409 with the type bookings_paused, and cancelling still works. Check it before you show times. The errors guide lists the calls.

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. You can also 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 at that time: the one whose visit is cheapest, then the one with the fewest bookings on that date, then one at random.

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 with a payment object is waiting for a deposit, not a code. Build a booking page shows how to take it.

The publishable key also needs a Turnstile token

Section titled “The publishable key also needs a Turnstile token”

A booking created with the publishable key must carry a Cloudflare Turnstile token, sent as the X-Turnstile-Token header. If the API cannot verify one, POST /v1/bookings answers 403 with turnstile_required. It is the same code whether the token is missing, invalid, solved on the wrong hostname, or Cloudflare itself is unreachable. The response does not distinguish those cases, deliberately, so do not build a client that tries to.

A secret key is unaffected: a booking created from your own server with sk_ needs no token at all.

This API does not issue Turnstile site keys to integrators, so a form you build cannot produce a token. For it, the workspace turns on “Custom booking frontend” in the dashboard. That turns the requirement off for the whole workspace, so anyone holding that workspace’s booking link can create bookings by script. The rate limits still apply, and so does a daily limit: each caller can only make so many bookings a day, and going over that answers daily_booking_limit (see the rate limits guide). Your own form hits both too.

A booking created from a server with a secret key needs no token, and takes no deposit.

A pending booking occupies the slot. With no payment object, 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.

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.

can_reschedule does not account for a pause. The same response carries online_bookings_paused. While that is true, rescheduling answers 409 bookings_paused even when can_reschedule is true, so offer Reschedule only when can_reschedule is true and online_bookings_paused is false. Cancelling is not affected.

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.

Your server sometimes needs to know about bookings it did not make. The customer cancelled from their email, or an operator moved it in the dashboard. That is what webhooks are for. When a delivery names a booking, GET /v1/bookings/{id} returns it.

If your receiver was down, GET /v1/bookings?updated_since=... lists every booking changed at or after that time, in pages. Every booking carries updated_at; keep the latest one you processed and send it back as updated_since to resume.