Availability & Booking
This guide shows the full booking workflow.
1. Create the schedule
from slotify import SlotGenerator
generator = SlotGenerator(
start="09:00",
end="17:00",
duration=30,
timezone="Asia/Kolkata",
)
2. Create the availability engine
from slotify import AvailabilityEngine
engine = AvailabilityEngine(
generator,
resource_id="doctor-123",
capacity=1,
)
3. Get bookable slots
available = engine.available_slots("2026-10-05")
for slot in available[:5]:
print(slot.start, "->", slot.end)
available_slots() starts with generated slots and filters them using booking policy, existing bookings, capacity, resources, and buffers.
Check a single slot
if engine.is_available(slot):
print("The slot is currently available")
Availability may change before the final reservation, so the application should treat reserve() as the final booking operation.
4. Reserve
booking = engine.reserve(
slot,
metadata={
"customer_id": "user-42",
"service": "consultation",
},
)
print(booking.booking_id)
Metadata is returned by booking.to_dict().
5. Reserve the first available appointment
booking = engine.reserve_first_available(
"2026-10-05",
"2026-10-09",
metadata={"customer_id": "user-42"},
)
This tries available slots until one is successfully reserved.
Capacity
One-person appointment:
engine = AvailabilityEngine(
generator,
resource_id="doctor-123",
capacity=1,
)
Ten-person class:
engine = AvailabilityEngine(
generator,
resource_id="class-1",
capacity=10,
)
The included in-memory store tracks overlapping confirmed bookings against the configured capacity.
Multiple resources
Use resource_ids when one appointment needs multiple resources.
engine = AvailabilityEngine(
generator,
resource_ids=("doctor-123", "room-4"),
)
An overlapping booking conflicts when it shares at least one resource.
This fits cases such as:
doctor + room
barber + chair
technician + machine
trainer + studio
Buffers
engine = AvailabilityEngine(
generator,
resource_id="chair-2",
buffer_before=10,
buffer_after=15,
)
Buffers do not change the appointment's displayed start/end. They expand the protected interval used for conflicts.
Booking policies
from datetime import timedelta
from slotify import AvailabilityEngine, BookingPolicy
policy = BookingPolicy(
minimum_notice=timedelta(hours=2),
maximum_horizon=timedelta(days=30),
cancellation_window=timedelta(hours=12),
reschedule_window=timedelta(hours=6),
max_reschedules=2,
)
engine = AvailabilityEngine(
generator,
resource_id="doctor-123",
capacity=1,
policy=policy,
)
Minimum notice
minimum_notice prevents booking too close to the current time.
minimum_notice=timedelta(hours=2)
Maximum horizon
maximum_horizon prevents booking too far into the future.
maximum_horizon=timedelta(days=30)
Blocked periods
from datetime import datetime
from slotify import BlockedPeriod, BookingPolicy
policy = BookingPolicy(
blocked_periods=(
BlockedPeriod(
start=datetime.fromisoformat("2026-10-10T10:00:00+05:30"),
end=datetime.fromisoformat("2026-10-10T14:00:00+05:30"),
reason="Doctor on leave",
),
),
)
Any overlapping slot is rejected by the policy.
Cancellation
Cancel by booking ID.
cancelled = engine.cancel(booking.booking_id)
print(cancelled.status)
The resulting status is cancelled.
An already-cancelled booking can be cancelled again safely through the engine.
A cancellation window can prevent last-minute cancellation:
BookingPolicy(
cancellation_window=timedelta(hours=12),
)
Rescheduling
Generate a target slot, then reschedule the existing booking.
new_slot = generator.generate_for_date("2026-10-06")[3]
updated = engine.reschedule(
booking.booking_id,
new_slot,
)
print(updated.booking_id)
print(updated.rescheduled_count)
The booking ID stays the same and rescheduled_count increases.
The new slot is checked against policy, capacity, resources, and conflicts.
Cancelled bookings cannot be rescheduled.
Recurring bookings
Create a weekly series from a template slot.
from slotify import RecurrenceRule
template_slot = generator.generate_for_date("2026-10-05")[0]
rule = RecurrenceRule(
weekdays=(0,),
count=8,
)
series = engine.reserve_recurring(
template_slot,
rule,
)
Python weekday numbering is Monday=0 through Sunday=6.
Every-two-weeks example:
rule = RecurrenceRule(
weekdays=(0,),
interval=2,
count=6,
)
Stop at a date:
rule = RecurrenceRule(
weekdays=(0,),
until="2026-12-28",
)
count and until cannot be used together.
Every booking in a series receives the same series_id.
If an occurrence cannot be reserved, occurrences already created by that series are cancelled before the error is returned.
Upcoming availability
For a rolling list of bookable future slots:
from datetime import datetime
available = engine.upcoming_available(
7,
now=datetime.fromisoformat("2026-10-05T08:00:00+05:30"),
)
Pass now explicitly for deterministic tests.
List bookings
Use the store to inspect bookings:
bookings = engine.store.list(
resource_id="doctor-123",
)
Filter a recurring series:
series_bookings = engine.store.list(
series_id=booking.series_id,
)
Cancelled bookings are excluded by default.
Include them explicitly:
all_bookings = engine.store.list(
include_cancelled=True,
)
iCalendar export
Export a booking to iCalendar text:
ics_text = booking.to_ics(
summary="Doctor Appointment",
description="30-minute consultation",
location="Room 4",
)
Your application can return the string as an .ics response or save it as a calendar file.
Recommended web request flow
GET /providers/123/availability
|
v
Django/DRF loads provider settings
|
v
SlotGenerator + AvailabilityEngine
|
v
Return available slots
|
v
User selects a slot
|
v
POST /bookings
|
v
reserve(slot)
|
+--> BookingConflictError
| if another request won the race
|
+--> Booking
Keep authentication, payments, notifications, and database records in your application.
Temporary holds
A hold is a short-lived reservation used while a customer is completing checkout or payment.
from datetime import datetime
hold = engine.hold(
slot,
expires_at=datetime.fromisoformat("2026-10-05T09:10:00+05:30"),
now=datetime.fromisoformat("2026-10-05T09:00:00+05:30"),
)
While the hold is active, the slot is unavailable.
assert not engine.is_available(
slot,
now=datetime.fromisoformat("2026-10-05T09:05:00+05:30"),
)
Confirm after successful payment:
booking = engine.confirm_hold(
hold.hold_id,
now=datetime.fromisoformat("2026-10-05T09:05:00+05:30"),
)
Release an abandoned checkout:
engine.release_hold(hold.hold_id)
The built-in InMemoryHoldStore automatically ignores expired holds. Production applications should implement HoldStore using durable storage and the same transaction/concurrency strategy as BookingStore.
A hold is a temporary claim; it is not a confirmed booking and should not be treated as payment success.
Resource selection strategies
Resource pools support three selection strategies.
First available
This is the default:
AvailabilityEngine(
generator,
resource_pool=("doctor-1", "doctor-2", "doctor-3"),
resource_strategy="first_available",
)
Resources are checked in the order supplied.
Round robin
AvailabilityEngine(
generator,
resource_pool=("doctor-1", "doctor-2", "doctor-3"),
resource_strategy="round_robin",
)
The engine rotates the starting resource for successive selections. The final reservation is still protected by the store's conflict check.
Least loaded
AvailabilityEngine(
generator,
resource_pool=("doctor-1", "doctor-2", "doctor-3"),
resource_strategy="least_loaded",
)
The engine counts overlapping confirmed bookings for each resource and tries the least-loaded resource first.
These strategies are allocation helpers, not business rules. Your application can still store its own provider qualifications, service restrictions, locations, and other routing rules.
Resource pools
When a customer can be served by any one member of a team, use resource_pool instead of requiring a specific resource.
engine = AvailabilityEngine(
generator,
resource_pool=("doctor-1", "doctor-2", "doctor-3"),
)
booking = engine.reserve_first_available("2026-10-05")
print(booking.resource_id)
Slotify checks the pool and chooses an available resource during reservation. The chosen resource is stored on the Booking.
Explain unavailable slots
Use check_availability() when your application needs a reason instead of a boolean:
result = engine.check_availability(slot)
if not result.available:
print(result.reason)
Possible reasons include booking-policy failures and resource/capacity conflicts.
Find availability more directly
First available slot:
slot = engine.next_available("2026-10-05", "2026-10-12")
Availability inside an exact datetime range:
slots = engine.available_between(
start_datetime,
end_datetime,
)
Availability for a requested appointment duration:
slots = engine.available_for_duration(
90,
"2026-10-05",
"2026-10-12",
)
Use limit to avoid generating more results than the application needs.
Booking limits
policy = BookingPolicy(
max_bookings_per_day=3,
max_bookings_per_week=10,
max_upcoming_bookings=5,
min_gap_between_bookings=timedelta(minutes=15),
)
These limits are checked against confirmed bookings in the configured store.
Named schedules
Use multiple SlotGenerator profiles when a provider has different availability modes:
engine = AvailabilityEngine.from_schedules(
{
"default": weekday_generator,
"evening": evening_generator,
"weekend": weekend_generator,
},
default_schedule="default",
)
slots = engine.available_slots(
"2026-10-05",
schedule_name="evening",
)
All schedules configured on an engine must use the same timezone.
Expanded recurrence
RecurrenceRule now supports daily, weekly, monthly, and yearly series.
RecurrenceRule(
frequency="monthly",
day_of_month=15,
count=6,
)
Skip dates without consuming the occurrence count:
RecurrenceRule(
frequency="weekly",
weekdays=(0,),
count=12,
excluded_dates=("2026-11-02",),
)