Get a key and make your first call
This guide makes one booking with curl. The base URL is
https://api.keptslot.com.
Read the glossary first if you have not. One noun here
does not mean what the dashboard calls it: staff is a bookable calendar,
not a person with a login.
What the workspace needs first
Section titled “What the workspace needs first”The API can only offer a time when three things exist:
- One calendar.
- Weekly hours on that calendar.
- One active service, assigned to that calendar.
A new workspace has none of them. Step 3 tells you which case you are in. Step 4 builds all three.
1. Create a secret key
Section titled “1. Create a secret key”- Open the dashboard and select your workspace.
- Go to Developer → API keys.
- Create a key of type Secret.
CAUTION: The full key value is shown one time. Store it now. A later read gives
null, and no support call can recover it. If you lose the key, revoke it and
create another one.
A secret key starts with sk_. Keep it on your server. Do not put it in a
browser. For the difference between the key types, read the
keys guide.
The samples below read the key from your shell, so that it stays out of your command history:
export KEPTSLOT_KEY="sk_your_secret_key_here"2. Make sure that the key works
Section titled “2. Make sure that the key works”Every call sends the key as a bearer token.
curl https://api.keptslot.com/v1/workspace \ -H "Authorization: Bearer $KEPTSLOT_KEY"A 200 returns the workspace settings. This sample is cut short. The real
response carries about thirty fields, and these are the ones that matter here:
{ "slug": "northside-barbers", "name": "Northside Barbers", "timezone": "America/Edmonton", "currency": "CAD", "slot_interval_minutes": 15, "require_booking_verification": false}If the response is a 401, the key is wrong or revoked. If the response is a
403 with secret_key_required, you sent a publishable key.
Note the timezone. Every time in this API is an RFC3339 timestamp, and the
responses use the timezone of the workspace. The appointment happens at the
business, so the offset is the business’s and not the customer’s.
Note slot_interval_minutes too. It is the step between start times, not the
length of an appointment. Step 5 shows what that does.
3. Ask what the workspace can book
Section titled “3. Ask what the workspace can book”A booking is always for exactly one service. Get the service id first.
curl https://api.keptslot.com/v1/services \ -H "Authorization: Bearer $KEPTSLOT_KEY"{ "data": [ { "id": "4aef5183-5995-44a7-846e-cae58ff60369", "name": "Cut and finish", "duration_minutes": 45, "price_cents": 4000, "active": true } ]}A service carries more fields than this, including the buffers, the lead time and the payment mode.
By default this endpoint shows only what a customer can book. A deactivated
service is not in this list, so every service here has active: true. To get
the deactivated services too, send include_inactive=true and read active.
POST /v1/services/{id}/activate makes a deactivated service bookable again.
What you do next depends on the list:
- The list has at least one service. Go to step 5 and use that
id. - The list is
{"data": []}. Do step 4 first.
4. Give the workspace something to book
Section titled “4. Give the workspace something to book”Do this step only if step 3 returned an empty list. It makes four calls. Keep the id that each one returns.
4a. Create a calendar.
curl -X POST https://api.keptslot.com/v1/staff \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Chair 1"}'{ "id": "4211e9e1-4df0-4859-956d-95d45faae1c9", "name": "Chair 1", "active": true, "title": null, "bio": null}4b. Give the calendar weekly hours. Without hours it offers no times.
Each block is one weekday and two clock times. weekday is 0 for Sunday
through 6 for Saturday. The two times are minutes from midnight, so 09:00 is
540 and 17:00 is 1020.
curl -X PUT https://api.keptslot.com/v1/staff/4211e9e1-4df0-4859-956d-95d45faae1c9/working-hours \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "working_hours": [ { "weekday": 1, "start_minute": 540, "end_minute": 1020 }, { "weekday": 2, "start_minute": 540, "end_minute": 1020 }, { "weekday": 3, "start_minute": 540, "end_minute": 1020 }, { "weekday": 4, "start_minute": 540, "end_minute": 1020 }, { "weekday": 5, "start_minute": 540, "end_minute": 1020 } ] }'The response echoes what you sent.
CAUTION: This call replaces the whole week. A block that you leave out is erased. Send every block that you want to keep, every time.
4c. Create a service.
curl -X POST https://api.keptslot.com/v1/services \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Cut and finish","duration_minutes":45,"price_cents":4000}'The response carries the new id, and a default for every field that you did
not send: no buffers, no lead time, 60 days of advance booking, and
payment_mode: "none".
4d. Assign the service to the calendar.
A service with no calendar offers no times, the same way a calendar with no hours does.
curl -X PUT https://api.keptslot.com/v1/services/4aef5183-5995-44a7-846e-cae58ff60369/staff \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"staff_ids":["4211e9e1-4df0-4859-956d-95d45faae1c9"]}'This call also replaces rather than adds. Send every calendar that performs this service.
5. Read the open slots
Section titled “5. Read the open slots”Ask for one service and one date range. The range must be 62 days or less. A
longer range answers 422 with validation_failed on to.
curl -G https://api.keptslot.com/v1/availability \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ --data-urlencode "service_id=4aef5183-5995-44a7-846e-cae58ff60369" \ --data-urlencode "from=2026-09-16T00:00:00-06:00" \ --data-urlencode "to=2026-09-19T00:00:00-06:00"The slots are grouped by calendar:
{ "data": [ { "staff_id": "4211e9e1-4df0-4859-956d-95d45faae1c9", "slots": [ { "start": "2026-09-16T09:00:00-06:00", "end": "2026-09-16T09:45:00-06:00" }, { "start": "2026-09-16T09:15:00-06:00", "end": "2026-09-16T10:00:00-06:00" }, { "start": "2026-09-16T09:30:00-06:00", "end": "2026-09-16T10:15:00-06:00" } ] } ]}These slots overlap, and that is correct. Each end is its start plus the
duration of the service, which is 45 minutes here. The starts are 15 minutes
apart, because slot_interval_minutes is 15. So the list is every start time
that the customer can choose. It is not a row of separate appointments. One
booking removes the others that overlap it.
An empty data array means that the range has no open slots. It is not an
error. If you have just done step 4 and the array is still empty, the range
holds no working day. The sample hours above are Monday to Friday.
6. Create the booking
Section titled “6. Create the booking”Send one start value from step 5. If you leave out staff_id, the API picks
an open calendar for you.
curl -X POST https://api.keptslot.com/v1/bookings \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \ -d '{ "service_id": "4aef5183-5995-44a7-846e-cae58ff60369", "starts_at": "2026-09-16T09:00:00-06:00", "customer": { "name": "Sam Okafor", "email": "sam@example.com", "phone": "+14035550123" } }'{ "id": "0182ed3e-a328-448e-9490-4309e1e03394", "staff_id": "4211e9e1-4df0-4859-956d-95d45faae1c9", "service_id": "4aef5183-5995-44a7-846e-cae58ff60369", "customer_id": "a6b5ee04-dfd6-4d02-8a2d-67925c0d1923", "starts_at": "2026-09-16T09:00:00-06:00", "ends_at": "2026-09-16T09:45:00-06:00", "status": "confirmed", "access_token": "Iuch7AB1mUlgHlGdRTy-sdny2OJVowbVFl8hAM2eS8U"}Two fields decide what happens next.
access_token is on this response and on no other. It is the customer’s manage
link. Store it now.
status is confirmed or pending. If the status is confirmed, you are
done. If the status is pending, go to step 7.
Send an Idempotency-Key on every create, as the sample does. A retry with the
same key and the same body returns the stored response instead of a second
booking. Read the rate limits guide for what a repeat
returns.
7. Verify a pending booking
Section titled “7. Verify a pending booking”A pending booking holds the slot and is not confirmed. The customer gets a
6-digit code by email. Send that code back.
curl -X POST https://api.keptslot.com/v1/bookings/0182ed3e-a328-448e-9490-4309e1e03394/verify \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "482915" }'{ "status": "confirmed" }Every refusal here returns the same error. A wrong code, an expired hold, too many attempts and an unknown booking are not told apart. The response is deliberately not an oracle for a 6-digit code.
Decide this step from the status on the create response. Do not decide it from
require_booking_verification on the workspace response. A business can change
that setting at any time.
Optional: fill in the booking page
Section titled “Optional: fill in the booking page”The steps above are enough to take a booking. The calls below set up the rest of what a customer sees on the booking page, except photos. Photos stay in the dashboard.
A calendar’s title and bio
Section titled “A calendar’s title and bio”The booking page shows a title and a short bio with the calendar’s name.
curl -X PATCH https://api.keptslot.com/v1/staff/4211e9e1-4df0-4859-956d-95d45faae1c9 \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"title":"Senior stylist","bio":"Twelve years of cutting and colour."}'{ "id": "4211e9e1-4df0-4859-956d-95d45faae1c9", "name": "Chair 1", "active": true, "title": "Senior stylist", "bio": "Twelve years of cutting and colour."}Every field is optional. A field that you leave out keeps its value. Send ""
to clear title or bio. POST /v1/staff also accepts title and bio.
Service categories
Section titled “Service categories”A category is a heading on the booking page’s list of services. Create one:
curl -X POST https://api.keptslot.com/v1/service-categories \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Hair"}'{ "id": "83ad1bfe-8111-4f84-9322-86c39fa6f596", "name": "Hair", "position": 0 }Then put a service in it:
curl -X PUT https://api.keptslot.com/v1/services/4aef5183-5995-44a7-846e-cae58ff60369/category \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"category_id":"83ad1bfe-8111-4f84-9322-86c39fa6f596"}'The response is the service, with the new category_id. Send
{"category_id":null} to take the service out of its category.
PUT /v1/services/{id} does not change the category. POST /v1/services
accepts category_id.
PUT /v1/service-categories/order sets the order. Send every category of the
workspace, each one time. A list with a category missing is rejected with
422. When you delete a category, its services stay and have no category.
Booking form questions
Section titled “Booking form questions”A question asks the customer for more than a name and contact details. Create one:
curl -X POST https://api.keptslot.com/v1/questions \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{ "label": "Hair length", "type": "single_select", "options": ["Short", "Medium", "Long"], "required": true, "applies_to": "selected_services" }'{ "id": "ec303d23-3064-4fa3-8cd4-77770a2b0eb0", "label": "Hair length", "help_text": null, "type": "single_select", "options": ["Short", "Medium", "Long"], "required": true, "applies_to": "selected_services", "service_ids": [], "position": 0, "active": true}With applies_to: "selected_services", the question is asked only for the
services that you set next. With "all_services", it is asked for every
service.
curl -X PUT https://api.keptslot.com/v1/questions/ec303d23-3064-4fa3-8cd4-77770a2b0eb0/services \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"service_ids":["4aef5183-5995-44a7-846e-cae58ff60369"]}'{ "service_ids": ["4aef5183-5995-44a7-846e-cae58ff60369"] }This call replaces the whole set. A service id that is not in this workspace
is dropped, and the call still returns 200. The response lists only what was
saved.
Four more rules:
PUT /v1/questions/{id}replaces the whole question. Send every field. Arequiredthat you leave out becomesfalse.PUT /v1/questions/orderaccepts a partial list, but a question that you do not list keeps its position number, and a tie sorts by creation order. To get an exact order, send every question.- A workspace has at most 20 active questions. Archive one with
POST /v1/questions/{id}/archiveto make room.POST /v1/questions/{id}/restoreis rejected with422when 20 are already active. - Answers that customers already gave keep the label and the answer from when they answered. Editing or archiving the question does not change them.
Extras
Section titled “Extras”An extra is a service that a customer adds to another service in the same visit. It is an ordinary service with two more fields:
standalonesays whether the service can be booked on its own. Left out, it istrue.extra_scopesays which services it can be added to:none,allorselected.allmeans every other active service that can be booked on its own.selectedmeans only the services inoffered_with_ids. Left out, it isnone.
A beard trim that can be booked alone and added to any service:
curl -X POST https://api.keptslot.com/v1/services \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Beard trim","duration_minutes":15,"price_cents":1500, "standalone":true,"extra_scope":"all"}'A gloss treatment that exists only as an extra to a colour service:
curl -X POST https://api.keptslot.com/v1/services \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Gloss treatment","duration_minutes":15,"price_cents":3000, "standalone":false,"extra_scope":"selected", "offered_with_ids":["b7d0c1f4-52a8-4c0e-9a13-6e41f2d85c70"]}'offered_with_ids is required when extra_scope is selected, and it is
ignored under all and none. Every service read returns it. A service that
is not booked on its own must have extra_scope of all or selected, and
only a service that is not booked on its own may take 0 minutes.
PUT /v1/services/{id} replaces the whole service. A replace that leaves out
standalone and extra_scope turns an extra back into a plain service.
To book a visit with extras, send the ids to GET /v1/availability as
extra_ids=id1,id2, and as an extra_ids array to POST /v1/bookings. Each
slot is as long as the service plus its extras, and only calendars that perform
all of them are listed. The booking response then carries extras and
price_cents, the price of the whole visit. At most 10 extras go on a visit,
each one time.
A calendar’s own price
Section titled “A calendar’s own price”A calendar can charge its own price for a service. Bookings on that calendar
are then priced at it instead of the service’s price_cents. Set it with
PUT /v1/services/{id}/staff/{staff_id}:
curl -X PUT https://api.keptslot.com/v1/services/4aef5183-5995-44a7-846e-cae58ff60369/staff/4211e9e1-4df0-4859-956d-95d45faae1c9 \ -H "Authorization: Bearer $KEPTSLOT_KEY" \ -H "Content-Type: application/json" \ -d '{"price_cents":5500}'{ "id": "4211e9e1-4df0-4859-956d-95d45faae1c9", "name": "Chair 1", "active": true, "price_cents": 5500}Send {"price_cents":null} to clear it. The calendar then charges the
service’s price.
The calendar must already be assigned to the service, or the call answers
404. A price is rejected with 422 on price_cents in four cases:
- The service has no price. Set the service’s
price_centsfirst. - The price is negative.
- The price is above 2147483647.
- The price is zero and the service’s
payment_modeisfullordeposit_percent.
Two changes to the service reach these prices. A PUT /v1/services/{id} that
clears price_cents clears every calendar’s price for that service. A
PUT /v1/services/{id} that sets payment_mode to full or
deposit_percent is rejected with 422 on price_cents while a calendar is
priced at zero for that service.
An extra is a service too, so a calendar can have its own price for an extra. A booking is priced for the calendar that gets it, the extras included.
Services and calendars that already exist
Section titled “Services and calendars that already exist”GET /v1/services/{id}/stafflists every calendar assigned to a service, deactivated ones included withactivefalse. Each entry carriesprice_cents, the calendar’s own price, which isnullwhen the calendar charges the service’s price. To add or remove one calendar, send the ids in this list back toPUT /v1/services/{id}/staffwith that change. Calendars that stay in the list keep their prices. A list that leaves out a deactivated calendar removes its assignment.PUT /v1/services/ordersets the order of the services. Send every service of the workspace, each one time, and include the deactivated ones: get the ids fromGET /v1/services?include_inactive=true. A list with a service missing is rejected with422.POST /v1/staff/{id}/activatemakes a deactivated calendar take bookings again. It counts against the plan’s calendars, as a new calendar does, so at the limit it is rejected with402andplan_limit_exceeded.
What to read next
Section titled “What to read next”- Build a booking page: the same flow from a browser on your own site.
- Receive and verify a webhook: how to hear about a booking you did not make.
- Errors: every error type, and the one error whose body changes with your key.