Skip to content
Keptslot
Start free

Read a booking

GET
/v1/public/bookings/{token}
curl --request GET \
--url https://api.keptslot.com/v1/public/bookings/example

This endpoint gives one booking and the business that holds it. The token in the path is the only credential. There is no login on this surface.

An unknown token, an expired token, and the token of a cancelled booking give the same 404. A cancellation clears the token. Then the booking resolves to no row, the same as a token that never existed.

Use can_cancel and can_reschedule to enable the buttons. Each one is true exactly when that call succeeds. They are not copies of each other. You can cancel a pending booking inside the notice window. You cannot move it.

Rate limited at 30 requests a minute for one caller address, shared across every route under /v1/public.

token
required
string

The booking and its business.

Media type application/json
object
booking
required
object
id
required
string format: uuid
status
required
string
starts_at
required

In the business’s timezone. The appointment happens at the business.

string format: date-time
ends_at
required
string format: date-time
duration_minutes
required

The visit’s length as booked, extras included.

integer
extras
required
Array<object>

One extra on a booking, as it was when the booking was made. A later change to the service does not change it.

object
service_id
required
string format: uuid
name
required
string
duration_minutes
required
integer
price_cents
required
integer | null
price_cents
required

The visit’s price as booked, extras included. Null when nothing in it had a price.

integer | null
service
required

The service booked, without its extras. Its price_cents is the price when it was booked, and its payment_amount_cents is what this booking was asked to pay, null when it took no payment. The bootstrap fields standalone, extra_scope and extra_ids are absent here. Its duration_minutes is the menu’s length now; the length as booked is booking.duration_minutes.

object
id
required
string format: uuid
name
required
string
duration_minutes
required
integer
price_cents

The price in the smallest currency unit. It is null when the business does not publish a price.

integer | null
description

Plain text written by the business. Render it as text. Do not render it as markup.

string | null
payment_mode

Whether this service takes a payment when it is booked, and of what kind. One of none, full, deposit_fixed or deposit_percent. Show the requirement before the customer fills in the form. A card is asked for after the booking is created, which is too late to be the first the customer hears of it.

string
payment_amount_cents

What the customer pays at booking time, in the smallest currency unit. It is computed for you: do not work it out from a percentage yourself, because the rounding rule lives on the server and two implementations of it will disagree.

It is null when payment_mode is none, and also when the service is misconfigured such that no amount can be computed. In that second case the service cannot be booked at all, so show it as unavailable rather than asking for a payment that will be refused.

integer | null
category_id

The heading this service groups under, matching an id in the bootstrap’s categories array. Null means uncategorised: put it in a final “Other” group, or in no group at all when that is the only group.

On PUT /v1/services/{id} this field is IGNORED. That endpoint is a full replace, and honouring it would mean a client that has never heard of categories strips a business’s grouping every time it syncs. Set it with PUT /v1/services/{id}/category, or send it on POST /v1/services.

string | null
standalone

Whether the service can be booked on its own. False means it is only ever added to another service as an extra.

boolean
extra_scope

Which services this one can be added to as an extra. none: it is not an extra. all: any other active service that can be booked on its own. selected: only the services the business chose.

string
Allowed values: none all selected
extra_ids

The extras a customer can add to this service, as ids of other services in this list. Empty when there are none.

Array<string>
staff
required

A bookable calendar. A calendar is not a person with a login. See the glossary.

object
id
required
string format: uuid
name
required
string
title

What this person does, such as “Senior stylist”. Plain text. Render it as text, never as markup. Null when the business has not written one – show the name alone and reserve no space.

string | null
bio

A short line about this person. Same rules as title.

string | null
customer
required
object
name
required
string
email
required
string
phone
required
string
can_cancel
required
boolean
cancel_cutoff_passed
required

True when the notice window is the reason can_cancel is false. For a past booking it is false, because the status is the reason instead.

boolean
cancel_cutoff_minutes
required

How long before the start time cancellation closes. A value of 0 means cancellation stays open until the start time.

integer
can_reschedule
required
boolean
payment

What has happened to the money for a booking that has already been paid for. It is absent when the booking took no payment.

This is the customer’s view. It carries no Stripe identifiers and no client secret: the payment is already made, and there is nothing left to confirm.

object
status
required

One of initializing, requires_payment, processing, succeeded, partially_refunded, refunded, refund_pending or canceled. A disputed payment stays succeeded: the money was taken, and a dispute is not a status.

string
amount_cents
required

A snapshot taken when the booking was made. Changing the service’s price later never changes it.

integer
currency
required
string
amount_refunded_cents
required

Zero when nothing has been refunded.

integer
refund_policy

The business’s own refund policy, word for word. We never parse it and we never compute a refund from it. Show it as written.

string
client_secret

Present only while status is requires_payment, so the customer can finish a payment they have not completed. Use it to mount Stripe’s Payment Element on connected_account_id.

This is what makes a payment resumable. The secret returned when the booking was created works once and is held only in that browser tab, so a refresh, a closed tab or a return from the customer’s bank would otherwise leave the customer unable to pay for a slot their own booking is still holding.

string
connected_account_id

Present under the same condition as client_secret.

string
stripe_publishable_key

Present under the same condition as client_secret. Construct Stripe.js with it.

string
payment_unavailable

True when the payment could be resumed but Stripe could not be reached to resume it. Absent otherwise.

Treat it as “try again shortly”, not as a failed payment and not as a lost booking. The slot is still held. It exists so this case is distinguishable from the other reasons no secret is present: initializing has no payment set up yet, and processing means the customer has already paid and must not be asked to pay again.

boolean
hold_expires_at

When the slot stops being held, present while the payment is still unsettled. Show the time. Do not compute it from a fixed number of minutes.

string format: date-time
workspace
required
object
slug
required
string
name
required
string
timezone
required
string
currency
required
string
vertical
required
string
publishable_key

Null when the workspace has no usable key. A customer can still view and cancel the booking. A reschedule needs the key, so it is not available.

string | null
blurb
string | null
terms_url
string | null
website_url
string | null
instagram_url
string | null
maps_url
string | null
address

The business’s own street address. Published by presence, the same as on the public bootstrap: null when the business has not set one.

string | null
confirmation_note

The same note the bootstrap carries. Show it once the booking is confirmed. A booking that takes a deposit redirects here to pay and never sees the booking flow’s confirmation screen, so this is the only page those customers can read it on. Plain text – render it as text, never as markup.

string | null
online_bookings_paused

True while the business has paused online bookings. False by default.

boolean
Example
{
"booking": {
"service": {
"extra_scope": "none"
}
}
}

There is no such resource in this workspace.

Media type application/json
object
error
required
object
type
required

A stable machine-readable code. Match on this.

string
message
required

A sentence for a person. Do not match on this.

string
param

The field that caused the refusal, if one field did.

string
dimension

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.

string
used

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.

integer format: int64
limit

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.

integer format: int64
Example generated
{
"error": {
"type": "example",
"message": "example",
"param": "example",
"dimension": "example",
"used": 1,
"limit": 1
}
}

This endpoint’s request budget is spent. The type field is rate_limited.

The budget is a token bucket, not a calendar-minute counter: it refills CONTINUOUSLY as time passes, a few tokens’ worth every second, rather than resetting all at once at the top of the minute. There is no wall-clock boundary to wait for. A REFUSED request costs nothing – the check happens before a token is spent, so hammering this endpoint while refused does not delay when it starts working again. What you get back is Retry-After: seconds until the bucket has refilled enough to admit one more request, computed from exactly how empty the bucket is right now. Retrying before then just gets refused again; retrying at or after Retry-After succeeds. See the endpoint’s own description for the actual numbers – they differ by endpoint and, for the publishable-key routes, by whether the budget is shared by the whole workspace or is one caller’s own share of it.

The response also carries RateLimit-Remaining (requests left in the bucket – always 0 on this response, since a refusal only happens once it reaches zero). Both headers are ADDITIVE: type alone is enough to act correctly, and a client that cannot read response headers loses nothing. Access-Control-Expose-Headers names both, so a browser-based integration on a publishable key can read them too, the same as it reads type from the body.

Media type application/json
object
error
required
object
type
required

A stable machine-readable code. Match on this.

string
message
required

A sentence for a person. Do not match on this.

string
param

The field that caused the refusal, if one field did.

string
dimension

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.

string
used

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.

integer format: int64
limit

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.

integer format: int64
Example generated
{
"error": {
"type": "example",
"message": "example",
"param": "example",
"dimension": "example",
"used": 1,
"limit": 1
}
}
Retry-After
integer

Seconds until this bucket admits another request.

RateLimit-Remaining
integer

Requests left in the bucket. Always 0 on this response.

A fault on our side. Do not retry the same request in a loop. The type field is internal_error.

Media type application/json
object
error
required
object
type
required

A stable machine-readable code. Match on this.

string
message
required

A sentence for a person. Do not match on this.

string
param

The field that caused the refusal, if one field did.

string
dimension

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.

string
used

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.

integer format: int64
limit

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.

integer format: int64
Example generated
{
"error": {
"type": "example",
"message": "example",
"param": "example",
"dimension": "example",
"used": 1,
"limit": 1
}
}