Skip to content

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.

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",),
)