Time zones
Availability is computed on the offer, never on the buyer. A schedule’s rules are written on a local clock; they are materialised into UTC day by day with the IANA database, so the day a clock changes has 23 or 25 hours and the rules still mean what they say locally.
- A rule’s day of the week is the local day. A “Monday” rule in Auckland covers Sunday evening in UTC.
- Durations are always absolute minutes. A 60 minute booking at 01:30 on the night of the change lasts 60 real minutes, whatever the wall clock says.
- A multi-day booking “Friday 10:00 to Monday 10:00” is computed locally and converted: it lasts 71 or 73 hours when it crosses a change.
Daylight saving
Section titled “Daylight saving”Band ends are converted with disambiguation: compatible:
- a non-existent time (02:30 on the March night) is pushed forward by the length of the jump, so 02:30 becomes 03:30 and the band keeps its wall-clock shape;
- a repeated time (02:30 on the October night) takes the first occurrence, the one still on summer offset.
Consequences, all under test: a 01:00-04:00 rule lasts 2 real hours on the spring-forward
day and 4 on the fall-back day, when the change falls inside the band; a 09:00-19:00 rule
lasts 10 real hours on both. A band whose two ends both fall inside the jump does not vanish:
it moves forward whole and keeps its wall-clock length. A band whose ends collapse onto the
same instant does vanish.
Slots inside a non-existent hour do not exist, not by a special rule, but because that hour is not in the timeline.
Rules, exceptions and blocks: the order
Section titled “Rules, exceptions and blocks: the order”For each local day:
- A
closedexception without hours suppresses the day: the bands its rules would have produced are not generated at all, night tail included. It does not touch the tail of a band that started the day before: a bar open Tuesday 22:00-02:00 and closed on Wednesday still serves until 2 a.m. on Tuesday night. - The bands of the rules whose
dayscontain the local weekday, and whose validity window (both ends inclusive) contains the day.validUntillimits the days a rule may start on, not the instants it produces. - The bands of that day’s
openexceptions, in addition to the rules, or alone if the day has no rules. On a day suppressed by step 1 they are suppressed too: closures beat openings. closedexceptions with hours are subtracted. They apply even on a suppressed day, because a closure can cross midnight onto a day that does open.- Blocks are subtracted whole: inside a block the capacity is 0, whatever the resource’s capacity.
- Each surviving segment carries the resource’s capacity.
Overlapping bands are unioned booleanly before capacity is applied: two rules covering the same instant open the resource once, not twice.
Other edge cases handled and tested
Section titled “Other edge cases handled and tested”- Two simultaneous requests for the last seat: one wins, the other gets
slot_unavailable. - A hold that expires between the availability call and the confirmation:
hold_expired. - A schedule change that invalidates a future booking: the booking is not touched; a
booking.orphanedevent is emitted. - A capacity reduction below existing bookings: same.
- Overlapping buffers: buffers are not bookable, but they constrain the admissible starts.
An existing occupancy carries its own buffers, not those of the service asking.
With
bufferSharing: truetwo buffers may overlap each other, never the body of the other booking. Blocks carry no buffer. - A booking across midnight, across a clock change, or across a closure in the middle.
- A quantity above one resource’s capacity but available across a group:
allowSplit: true. - Slots partly covered by a block: eroded correctly.
- Availability over huge windows: capped at 90 days per call, 7 with
explain. - A booking in the past: refused.
- A customer in another time zone: availability is computed on the resource and presented in the zone asked for.
- A pricing rule on the hour the clocks change: see below.
Prices that depend on the clock
Section titled “Prices that depend on the clock”pricingRules are read on the local clock of the offer, on the start of the slot. Four
cases follow from that, and all four are tested:
- A band that crosses midnight.
timeFrom: '22:00', timeTo: '02:00'is half open and wraps: it covers 22:00 to 23:59 and 00:00 to 01:59, and not 02:00 itself. Combine it withdaysand the wrap becomes visible: the day is the day the slot starts on, sodays: ['fri']on that band covers Friday 22:30 and not Saturday 00:30. A night rate is written withoutdays, or with both days it touches (['fri', 'sat']). - The night the clocks go forward. No instant reads 02:30 local, so a rule about
[02:00, 03:00)matches nothing that night. The hour does not exist; a rule about it cannot fire, and the slots on either side keep the price they would have had. - The night the clocks go back. Two instants read 02:30 local, and the rule matches both. The hour happens twice, and both times cost what the rule says.
- A local day that is not the UTC day.
days: ['sat']is about the Saturday of the club. A slot at 00:30 on Saturday in Rome is 22:30 on Friday in UTC and is priced as Saturday; the same rule in Auckland moves the other way. The zone is the offer’s, never the caller’s.
Two more things worth knowing before you write a rule: the first matching rule wins and
nothing chains after it, and priceAdd never takes a price below zero. The price a slot shows
is the price the booking freezes, so a rule changed afterwards does not move a booking that has
already been made.
The same rules, over HTTP
Section titled “The same rules, over HTTP”Everything above is the engine. This is what it looks like from the API, and the three habits that keep an integration out of trouble.
Instants in carry an offset, instants out are UTC
Section titled “Instants in carry an offset, instants out are UTC”from, to and start are ISO 8601 with an explicit offset. A bare date is refused, and the
error says why rather than guessing for you:
Error [parameter_invalid] --from must be an ISO 8601 instant with an explicit offset, got "2026-09-14". param: from Fix: Write it as `2026-09-08T07:00:00Z` or `2026-09-08T09:00:00+02:00`. A bare date is refused because midnight is not the same instant in every time zone.Every instant in a response is UTC with a Z. Format it for the reader at the edge of your
system, never in the middle of it.
timezone is presentation, and only presentation
Section titled “timezone is presentation, and only presentation”POST /v1/availability takes an optional timezone, and bookrail availability --tz sets it.
It changes the local column of the answer and nothing else. The same window asked twice, once
in Europe/Rome and once in Asia/Tokyo, returns the same UTC instants:
[test] 16 slot(s), times shown in Asia/Tokyo
start (UTC) local end (UTC) min cap price------------------------ ---------------- ------------------------ --- --- ---------2026-09-14T06:00:00.000Z 2026-09-14 15:00 2026-09-14T07:00:00.000Z 60 1 30.00 EUR2026-09-14T06:30:00.000Z 2026-09-14 15:30 2026-09-14T07:30:00.000Z 60 1 30.00 EURThose are the same instants a request in Europe/Rome returns, at 08:00 and 08:30 local. The
grid belongs to the court, not to whoever is asking.
A booking stores the zone it was sold in (timezone on the booking), so a confirmation can be
rendered in the local time of the offer without guessing later.
The window has limits, and they are 400s, never 500s
Section titled “The window has limits, and they are 400s, never 500s”- A window wider than 90 days is
400 parameter_invalid. explainis capped at 7 days, because it materialises a row per rejected instant.GET /v1/availability/nextsearches up to 90 days ahead and answers with the first bookable instant, which is usually the call you want instead of paging through a month.
Reading a schedule back
Section titled “Reading a schedule back”bookrail pull writes the current project out as a bookrail.config.ts, with the schedules in
local time exactly as they are stored, and bookrail diff tells you whether a file and a
project agree. Neither converts anything to UTC on the way, because a schedule written in UTC
is a schedule that will be wrong in six months.