How the pieces fit
You never write a slot. Slots are what is left after the rules meet the
calendar, so they are always derived and never stored.
List and publish links
Times are a clock, days are numbers.
"10:00" rather than 600 minutes past
midnight, because nobody writes an integration in minutes since midnight. Days
stay numeric with Sunday at 0 — that is what EXTRACT(DOW) and JavaScript’s
getDay() both use, and inventing a third vocabulary would mean translating
twice.end_time may be "24:00", meaning midnight at the far end of the day. 00:00
would read as the start of the same day and close the window before it opened.400 and says nothing about
who holds it.
A window too short to hold one meeting is refused with a sentence naming the
problem, not a constraint name: “The window from 09:00 to 09:30 is shorter than
the 60-minute meeting it is meant to hold.”
Read, change, close
{id} may be the slug. It is the half you already have — it is in the URL
you published — so a request whose only purpose is translating one identifier
into another is not needed.
PATCH changes only the fields you name, and the three rule fields are checked
together against the result: shortening the window and shortening the meeting
are each fine alone and impossible in the wrong order.
The slots a link is offering
bookable_between is why an empty list is not always “fully booked”.A link’s own lead_minutes and horizon_days bound what it will ever offer.
Ask for next year and you get nothing — not because the day is busy, but because
the page is not open that far ahead. Those two edges are reported so you can
tell the difference.Take a booking
starts_at must be exactly one of the offered slots — take it from
/slots rather than constructing it. A time that is not on offer is refused
with the next one that is:
Being told about bookings
Subscribe tobooking.created and booking.cancelled
rather than polling GET /v1/bookings on a timer.
Both routes raise them — a guest on /book/{slug} and a call to this API —
because the event is raised when the booking row appears rather than by either
caller. Since nearly every booking comes from the public page, an integration
that only watched its own writes would miss almost all of them.
Personal booking links raise nothing, for the same reason a key cannot read
them. See the warning on the Webhooks page.
List and cancel
Cancelled bookings are hidden by default: a cancelled meeting is not on anybody’s
calendar, so including it would answer “what is booked” with things that are not.
Cancelling gives the time back.Cancelling something already cancelled succeeds and changes nothing. You asked
for it to be cancelled, and it is.
DELETE /v1/bookings/{id} stamps cancelled_at, marks the calendar event
cancelled, and puts the slot back on offer — the same time can be booked
again by somebody else.The record is kept rather than deleted, because “who booked this and then
cancelled” is a real question. The event is marked cancelled rather than
removed, so the owner can still see what was in their day and who called it
off; deleting it would make the meeting vanish with no trace, which is how
somebody turns up anyway.Not available yet
The public booking page itself is not an API./book/{slug} is a page for
people. A guest booking through it does not need a key, and this API is the
owner’s side of the same data.
Confirmation and reminder emails are sent by the app on the schedule it
already uses. There is no way to trigger, suppress or re-send one here.
Rescheduling is a cancel and a new booking. There is no single call that
moves one, because the new time has to be checked against the slots exactly as
a first booking would be.
Errors
See Errors for the full response shape.