# How booking works

Booking pages, weekly availability, how open slots are calculated in the host's timezone, the booking lifecycle for direct and approval pages, questions, and private pages.

Four things make up every booking: a **booking page** (what can be booked), the host's **availability** (when), the **slots** clndr.pro derives from those two, and the **booking** a guest makes in one of them. This page explains each one well enough that the API's answers stop surprising you.

## Booking pages

A booking page is one bookable meeting type: "30-minute intro call", "Portfolio review". Hosts create them under [**Booking pages**](https://www.clndr.pro/booking) in the dashboard, and the API can read them but not create or edit them.

| Field                     | Dashboard label              | What it does                                                                                                |
| ------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `slug`                    | Link                         | Identifies the page in URLs and API paths. Unique per account.                                              |
| `duration_minutes`        | Duration (minutes)           | The length of every slot, 1 to 480.                                                                         |
| `booking_type`            | Confirmation                 | `direct` (*Instant*) or `approval` (*Needs approval*). See [Booking types](#booking-types).                 |
| `buffer_time_minutes`     | Buffer time between bookings | The gap between one generated slot and the next.                                                            |
| `max_days_ahead`          | Bookable up to               | How far out guests can book. Later days have no slots.                                                      |
| `visibility`              | Page Visibility              | `public`, `private` or `team`. See [Private and team pages](#private-and-team-pages).                       |
| `availability_set_id`     | Hours                        | Which of the host's schedules applies. Empty means all of them.                                             |
| `auto_generate_meet_link` | Add a Google Meet link       | Confirmed bookings get a Google Calendar event with a Meet link.                                            |
| `show_on_profile`         | List on your public profile  | Lists the page at `www.clndr.pro/{username}`.                                                               |
| `is_active`               |                              | Inactive pages still appear in List booking pages, but can't be fetched, have no slots and can't be booked. |

`description` is Markdown the host writes. Render it as Markdown: the React components already do.

## Availability

A host's availability is a set of weekly rules: "Monday 09:00–12:00", "Monday 14:00–17:30", "Thursday 09:00–12:00". A day can have several windows. Hosts can keep several sets (say "Office hours" and "Evenings") and point each booking page at one of them, or at none, in which case every set counts.

The times are wall-clock times in the host's timezone, the `timezone` on their profile (an IANA name such as `Europe/London`). "09:00" means 09:00 where the host is, through daylight-saving changes. There are no one-off exceptions or holidays yet; a host blocks a day by putting an event in their calendar.

## Slots

[List open slots](/api-reference/operations/listSlots) returns the slots a guest can book on one day. clndr.pro builds them like this:

1. Take the host's calendar day for the `date` you asked for. `date=2026-10-15` means 15 October **in the host's timezone**.
2. For each availability window that day, place a slot at the window's start, then the next one `duration_minutes + buffer_time_minutes` later, until a slot would run past the window's end.
3. Drop slots that overlap a pending or confirmed booking, or an event on the host's calendar. Google Calendar events count once they've synced into clndr.pro. Only events that start and end within that day are checked, so an overnight or multi-day event doesn't block the day.
4. Drop slots that have already started, and return nothing at all for days past `max_days_ahead`.

Every slot comes back as a UTC `start` and `end`:

```json
{ "data": [{ "start": "2026-10-15T08:00:00.000Z", "end": "2026-10-15T08:30:00.000Z" }] }
```

To book one, send `start` and `end` back unchanged as `startTime` and `endTime`.

## Timezones, worked through

The host is in London, with Thursday availability 09:00–12:00 and a 30-minute page with a 10-minute buffer. On Thursday 15 October 2026 London is on summer time, UTC+1.

| Slot (host, London) | `start` (UTC) | Guest in New York (UTC−4) | Guest in Tokyo (UTC+9) |
| ------------------- | ------------- | ------------------------- | ---------------------- |
| 09:00–09:30         | `08:00Z`      | 04:00 Thu                 | 17:00 Thu              |
| 09:40–10:10         | `08:40Z`      | 04:40 Thu                 | 17:40 Thu              |
| 10:20–10:50         | `09:20Z`      | 05:20 Thu                 | 18:20 Thu              |
| 11:00–11:30         | `10:00Z`      | 06:00 Thu                 | 19:00 Thu              |

The next slot would be 11:40–12:10, which runs past 12:00, so there isn't one.

Now move the window to 15:00–17:00 London. The last slots are 23:00 and 23:40 Thursday for the Tokyo guest, and 00:20 **Friday**. One call for the host's Thursday returned a slot on the guest's Friday. Two rules keep this sane in your UI:

* Format every time in the guest's timezone (`Intl.DateTimeFormat` with no `timeZone`, or `toLocaleTimeString()`, does that in a browser).
* If you show a calendar in the guest's time, group slots by their local date rather than by the `date` you requested. For a full week, request each day and merge the results.

## Bookings

A booking is a guest's claim on a slot. Create one with [Create a booking](/api-reference/operations/createBooking), from a publishable or a secret key. clndr.pro checks that the time doesn't overlap another pending or confirmed booking for the host. A taken time fails with `409`; fetch the slots again and let the guest pick another.

A publishable key can only book a slot the slots endpoint is offering right now: anyone can copy that key out of your page, so a booking made with it must be in the future, inside the host's hours, within `max_days_ahead`, at the page's duration. When it isn't (a taken slot included), the message is `This time slot is not available. Fetch the slots again and pick another.` A secret key, which only your server holds, can book any time that doesn't overlap another booking; a taken time there reads `This time slot is no longer available. Please pick another.`

One guest email address gets five booking attempts per clock hour (`429` past that; it resets at the top of the hour).

### Booking types

```mermaid title="Booking lifecycle"
stateDiagram-v2
  [*] --> confirmed: direct page
  [*] --> pending: approval page
  pending --> confirmed: host confirms
  pending --> cancelled: host declines
  confirmed --> cancelled: host cancels
```

**Direct** pages (*Instant* in the dashboard) confirm on the spot. The guest gets a confirmation email from clndr.pro, the host gets a new-booking email, and if the page auto-generates Meet links, a Google Calendar event is created on the host's calendar and Google sends the guest an invite.

**Approval** pages create a `pending` booking. The host gets a booking-request email. The guest gets nothing yet, so tell them in your UI that their request was sent, not that they're booked. The slot is held: no one else can book it while it's pending.

The host then confirms or declines, in the dashboard under [**Meetings**](https://www.clndr.pro/meetings) or through [Confirm or cancel a booking](/api-reference/operations/updateBooking). The two paths notify differently:

|         | Dashboard                                                                       | API (`PATCH` / `DELETE`)                                                            |
| ------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Confirm | Confirmation email to the guest, plus the Google event and Meet link if enabled | Google event and Meet link if enabled (Google sends the invite); no clndr.pro email |
| Cancel  | Cancellation email to the guest; the Google event is deleted                    | The Google event is deleted and Google tells the guest; no clndr.pro email          |

If you confirm or cancel through the API on a page without Meet links, nobody hears about it unless you email the guest yourself. clndr.pro doesn't send reminder emails yet.

### Google Calendar status

`sync_status` on a booking records whether its Google Calendar event was created: `synced`, `failed` (with the reason in `sync_failure_reason`, often `Google Calendar is not connected`), or `not_synced` when no event was needed. It's written when the guest books and when the host confirms or cancels in the dashboard. Confirming or cancelling through the API (`PATCH`, `DELETE`) doesn't update it, so after those, look at `google_calendar_event_id` and `google_meet_link` instead. A failed Google call never blocks the booking itself.

## Questions and answers

Each page can ask guests questions, returned with the page by [Get a booking page](/api-reference/operations/getBookingPage), in `order_index` order.

| `question_type` | Render as                                          |
| --------------- | -------------------------------------------------- |
| `text`          | A single-line text input                           |
| `email`         | An email input                                     |
| `phone`         | A `tel` input                                      |
| `textarea`      | A multi-line text area                             |
| `select`        | A dropdown of `options`                            |
| `checkbox`      | A checkbox; send its answer as text, such as `Yes` |

The dashboard creates the first four; `select` and `checkbox` exist in the data model for pages set up other ways.

Send answers as `responses: [{ questionId, answer }]`, with every answer as a string. Two things the API leaves to you: it doesn't reject a booking that skips a question marked `is_required`, so enforce that in your form; and every `questionId` must belong to the page, because one unknown id means none of that booking's answers are saved. Only send ids from the page you fetched.

## Private and team pages

`visibility` controls who can open the hosted page at `www.clndr.pro/{username}/{slug}`:

* **public**: anyone with the link.
* **private**: visitors need an access code. With **Ask visitors for a code** on, the page asks for one; with it off, the page looks like it doesn't exist. Hosts create codes on the page's edit screen. They look like `K7Q2-M9XD-4TPA-8WNE`, are case-insensitive, and expire after a period you choose. A link with the code (`?c=K7Q2-M9XD-4TPA-8WNE`) opens the page directly.
* **team**: only signed-in members of the chosen team can book.

These checks protect the hosted page and the embeds. API keys skip them, because a key acts as the page's owner: it lists, reads and books private and team pages like any other. See [Choose an integration](/integration-options#private-and-team-pages) for what that means for your site.
