Create a booking
const url = 'https://api.keptslot.com/v1/bookings';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"service_id":"3a7b1e90-2c4d-4f81-9b02-7e5c1a6d8f44","starts_at":"2026-09-01T09:00:00+01:00","customer":{"name":"Sam Okafor","email":"sam@example.com","phone":"+447700900123"}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.keptslot.com/v1/bookings \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "service_id": "3a7b1e90-2c4d-4f81-9b02-7e5c1a6d8f44", "starts_at": "2026-09-01T09:00:00+01:00", "customer": { "name": "Sam Okafor", "email": "sam@example.com", "phone": "+447700900123" } }'This endpoint books an appointment. Both key types can call it, because the publishable key drives the public booking widget.
Read the status on the response to decide what happens next. If the
status is pending, the customer must enter a 6-digit code, and you
send that code to POST /v1/bookings/{id}/verify. Do not decide this
from require_verification on the bootstrap response.
access_token is on this response only. It is the customer manage
link. Store it now, because no later response repeats it.
If staff_id is absent, the API picks an available calendar.
Authorizations
Section titled “ Authorizations ”Parameters
Section titled “ Parameters ”Header Parameters
Section titled “ Header Parameters ”Makes a retry safe. The first request stores its response. A repeat with the same key and the same body gives that stored response again, with the same status code.
If you send the same key with a different body, the endpoint refuses the request with a 422.
Only a 2xx response is stored. Two requests that arrive at the same time do not replay: the loser gets its own error.
Request Body required
Section titled “ Request Body required ”object
The calendar to book. If it is absent, the API picks an available calendar.
An RFC3339 timestamp. It must be an open slot.
object
E.164 format. It is optional for every workspace. If it is absent, the customer is notified by email only.
Answers to the custom questions of the booking form. It is optional. A caller that does not use questions omits it.
object
Example
{ "service_id": "3a7b1e90-2c4d-4f81-9b02-7e5c1a6d8f44", "starts_at": "2026-09-01T09:00:00+01:00", "customer": { "name": "Sam Okafor", "email": "sam@example.com", "phone": "+447700900123" }}Responses
Section titled “ Responses ”The booking is created.
object
The customer’s manage link token. It is present only on the response that creates the booking. Store it then. No later response repeats it.
When this booking stops holding its slot. It is present only while the booking is on a hold, and absent otherwise.
Show this time rather than counting a fixed number of minutes from now. The window differs between a booking waiting on a code and one waiting on a card, and it is longer for a card because a 3DS redirect goes to the customer’s bank and back.
Present only when the service takes a payment, and only on the response that creates the booking. The key is absent for a service that takes no payment. It is never null, so a client can test for the key itself.
Use these fields to mount Stripe’s Payment Element on the connected account and confirm the payment. The client secret works once, for this booking.
object
What this booking costs, in the smallest unit of the currency. It is a snapshot taken when the booking was made. Changing the service’s price later never changes it.
The business’s Stripe account. Pass it as stripeAccount when you
construct Stripe.js.
Example generated
{ "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "staff_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "service_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "customer_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "starts_at": "2026-04-15T12:00:00Z", "ends_at": "2026-04-15T12:00:00Z", "status": "example", "access_token": "example", "hold_expires_at": "2026-04-15T12:00:00Z", "payment": { "client_secret": "example", "amount_cents": 1, "currency": "example", "connected_account_id": "example" }}The key is missing, malformed or revoked.
object
object
A stable machine-readable code. Match on this.
A sentence for a person. Do not match on this.
The field that caused the refusal, if one field did.
The plan limit that refused the request. It is present only on a 402, and only for a secret key or an account key. See the errors guide.
The current count for that dimension. It is present only on a 402, and only for an account key. It is absent when the refusal has no count to report. Absent and zero are different answers.
The cap for that dimension. It is present only on a 402, and only for an account key. A cap of zero is a real value.
Example generated
{ "error": { "type": "example", "message": "example", "param": "example", "dimension": "example", "used": 1, "limit": 1 }}A plan limit refused the request. The fields in the body change with the key type. See the errors guide.
object
object
A stable machine-readable code. Match on this.
A sentence for a person. Do not match on this.
The field that caused the refusal, if one field did.
The plan limit that refused the request. It is present only on a 402, and only for a secret key or an account key. See the errors guide.
The current count for that dimension. It is present only on a 402, and only for an account key. It is absent when the refusal has no count to report. Absent and zero are different answers.
The cap for that dimension. It is present only on a 402, and only for an account key. A cap of zero is a real value.
Example generated
{ "error": { "type": "example", "message": "example", "param": "example", "dimension": "example", "used": 1, "limit": 1 }}The time is no longer available.
object
object
A stable machine-readable code. Match on this.
A sentence for a person. Do not match on this.
The field that caused the refusal, if one field did.
The plan limit that refused the request. It is present only on a 402, and only for a secret key or an account key. See the errors guide.
The current count for that dimension. It is present only on a 402, and only for an account key. It is absent when the refusal has no count to report. Absent and zero are different answers.
The cap for that dimension. It is present only on a 402, and only for an account key. A cap of zero is a real value.
Example generated
{ "error": { "type": "example", "message": "example", "param": "example", "dimension": "example", "used": 1, "limit": 1 }}The request body or the query is not valid.
object
object
A stable machine-readable code. Match on this.
A sentence for a person. Do not match on this.
The field that caused the refusal, if one field did.
The plan limit that refused the request. It is present only on a 402, and only for a secret key or an account key. See the errors guide.
The current count for that dimension. It is present only on a 402, and only for an account key. It is absent when the refusal has no count to report. Absent and zero are different answers.
The cap for that dimension. It is present only on a 402, and only for an account key. A cap of zero is a real value.
Example generated
{ "error": { "type": "example", "message": "example", "param": "example", "dimension": "example", "used": 1, "limit": 1 }}Two different refusals share this status, and they are told apart
by error.type. Do NOT treat them the same way.
rate_limited is the request limiter. It counts per calendar
minute, a refused request still counts, and retrying after the
minute turns over succeeds. See the rate-limit guide.
booking_volume_refused means this account has created an
implausible number of bookings in its current billing period. It
is a platform safety limit, not a plan allowance: no plan raises
it, no plan is exempt from it, and it is not something an upgrade
resolves. Retrying does not help. The limit resets when the
account’s billing period does, which may be weeks away, so a
client that retries on a backoff will simply keep being refused.
There is deliberately no Retry-After, no number in the body, and
no dimension/used/limit — an account reaching this has a
runaway integration, and the fix is to stop it. If you believe
you have reached this legitimately, contact support.
object
object
A stable machine-readable code. Match on this.
A sentence for a person. Do not match on this.
The field that caused the refusal, if one field did.
The plan limit that refused the request. It is present only on a 402, and only for a secret key or an account key. See the errors guide.
The current count for that dimension. It is present only on a 402, and only for an account key. It is absent when the refusal has no count to report. Absent and zero are different answers.
The cap for that dimension. It is present only on a 402, and only for an account key. A cap of zero is a real value.
Example generated
{ "error": { "type": "example", "message": "example", "param": "example", "dimension": "example", "used": 1, "limit": 1 }}