Skip to content
Keptslot
Start free

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.

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. Open the dashboard and select your workspace.
  2. Go to Developer → API keys.
  3. 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:

Terminal window
export KEPTSLOT_KEY="sk_your_secret_key_here"

Every call sends the key as a bearer token.

Terminal window
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.

A booking is always for exactly one service. Get the service id first.

Terminal window
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.

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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

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.

Terminal window
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.

Send one start value from step 5. If you leave out staff_id, the API picks an open calendar for you.

Terminal window
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.

A pending booking holds the slot and is not confirmed. The customer gets a 6-digit code by email. Send that code back.

Terminal window
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.

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.

The booking page shows a title and a short bio with the calendar’s name.

Terminal window
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.

A category is a heading on the booking page’s list of services. Create one:

Terminal window
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:

Terminal window
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.

A question asks the customer for more than a name and contact details. Create one:

Terminal window
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.

Terminal window
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. A required that you leave out becomes false.
  • PUT /v1/questions/order accepts 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}/archive to make room. POST /v1/questions/{id}/restore is rejected with 422 when 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.

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:

  • standalone says whether the service can be booked on its own. Left out, it is true.
  • extra_scope says which services it can be added to: none, all or selected. all means every other active service that can be booked on its own. selected means only the services in offered_with_ids. Left out, it is none.

A beard trim that can be booked alone and added to any service:

Terminal window
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:

Terminal window
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 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}:

Terminal window
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_cents first.
  • The price is negative.
  • The price is above 2147483647.
  • The price is zero and the service’s payment_mode is full or deposit_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.

  • GET /v1/services/{id}/staff lists every calendar assigned to a service, deactivated ones included with active false. Each entry carries price_cents, the calendar’s own price, which is null when the calendar charges the service’s price. To add or remove one calendar, send the ids in this list back to PUT /v1/services/{id}/staff with 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/order sets the order of the services. Send every service of the workspace, each one time, and include the deactivated ones: get the ids from GET /v1/services?include_inactive=true. A list with a service missing is rejected with 422.
  • POST /v1/staff/{id}/activate makes 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 with 402 and plan_limit_exceeded.