Read a booking page
const url = 'https://api.keptslot.com/v1/public/workspaces/example';const options = {method: 'GET'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url https://api.keptslot.com/v1/public/workspaces/exampleThis endpoint gives one slug’s workspace, its bookable services and calendars, and the questions its booking form asks.
It carries no API key. The response includes the workspace’s own
publishable key, which the Keptslot booking page uses. That key allows
no other origin, so a page on your own site that sends it is refused
with 403 origin_not_allowed. Send your own publishable key instead.
require_verification warns the customer that a 6-digit code follows.
Do not use it to decide the next screen. Use the status on the create response instead. A business can change this setting at any time.
Rate limited at 30 requests a minute for one caller address, shared
across every route under /v1/public.
Parameters
Section titled “ Parameters ”Path Parameters
Section titled “ Path Parameters ”Responses
Section titled “ Responses ”The workspace and its bookable content.
object
object
An IANA zone name, such as Europe/London.
The workspace’s own pk_ key, used by the
Keptslot booking page. A page on your own site
sends its own key instead. It is public by design,
not a secret.
True when the business sends text messages as well as email. The phone field is optional for every business, whatever this value is.
Selects the customer-facing words, such as
appointment or meeting. It changes copy only. It
never changes booking mechanics.
The business’s own street address. Published by presence, the same as contact_email and contact_phone: null when the business has not set one.
A short line about the business, shown under its name. Plain text. Render it as text, never as markup. Null when the business has not written one, which is the usual case.
The BUSINESS’s own terms, not the platform’s. Show
it alongside ours and label it so a customer can
tell whose is whose. Always http or https.
Shown on the confirmation screen after the booking succeeds. What to bring, where to park, the door code. The confirmation, reschedule and reminder emails carry it too. Plain text – render it as text, never as markup. Null is the usual case and renders nothing at all.
For the link card only, when someone pastes the booking link into a message. Crawlers do not run JavaScript, so nothing rendered in a browser can use this. Fall back to the business name when it is null.
False when the business does not let customers pick
who they see. Omit the staff step and send no
staff_id. The create endpoint already treats
staff_id as optional, so nothing else changes.
True while the business has paused online bookings. A custom frontend reads this to tell a paused workspace from one with no free times. False by default.
The business’s refund terms, for disclosure BEFORE a customer picks a service that takes a deposit. Plain text, passed through verbatim. We never parse it and never compute a refund from it.
object
The price in the smallest currency unit. It is null when the business does not publish a price.
Plain text written by the business. Render it as text. Do not render it as markup.
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.
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.
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.
Whether the service can be booked on its own. False means it is only ever added to another service as an extra.
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.
The extras a customer can add to this service, as ids of other services in this list. Empty when there are none.
object
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.
A short line about this person. Same rules as title.
Absent when the calendar has no photo.
The services this calendar performs. Offer it only for these. Empty means it is offered for none.
The services this calendar charges its own
price for. A service it performs that is not
listed costs the service’s price_cents.
Empty when it has no prices of its own. Use
GET /v1/quote for a visit’s figures.
object
The headings services group under, in the order the business set. Always an array, never null. Empty when the business has created none, which renders as no grouping at all. Do not re-sort it: the order is the business’s and is decided on the server.
A service whose category_id is null, or names no
category here, belongs in a final “Other” group – or in
no group at all when that is the only group.
object
object
Plain text written by the business. Render it as text.
short_text is one line and long_text several. single_select
is one choice from options, which is empty for the other two.
The scope of the question. If the value is selected_services,
then service_ids lists the services it applies to.
Example
{ "services": [ { "extra_scope": "none" } ], "questions": [ { "type": "short_text" } ]}There is no such resource in this workspace.
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 }}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.
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 }}Headers
Section titled “ Headers ”Seconds until this bucket admits another request.
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.
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 }}