Skip to main content

Availability rules

As the name suggests, availability rules are used to define the availability of a service. They are used to define the time slots in which the service can be booked.

{
// Event duration (legacy - use availabilitySchedule instead)
"duration": "string",
// Start frequency (legacy - use availabilitySchedule instead)
"step": "string",
// Schedule - Legacy approach
"repeatingAvailability": {
"timeZone": "string",
"weekly": {
"[DAY_OF_THE_WEEK]": [
{
"start": "string",
"end": "string"
}
]
}
},
// Schedule - Recommended approach (takes precedence over repeatingAvailability)
"availabilitySchedule": {
"schedule": {
"defaultDurations": { "type": "discrete", "values": ["PT1H"] },
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "09:00",
"duration": "PT8H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]
}
}
]
}
},
// Maximum concurrent bookings
"maxConcurrentBookings": 0,
// Minimum booking notice
"minBookingNotice": "string",
// Maximum booking notice
"maxBookingNotice": "string",
// Time margin after the booking
"beforeBuffer": "string",
// Time margin before the booking
"afterBuffer": "string",
// Allows connecting external calendars and creating or checking availability in them...
"thirdPartyCalendars": {},
// Allows manually configuring intervals where availability will not be offered...
"busyIntervals": [],
}

Basic rules

Duration (duration)

The duration field allows you to configure the duration of the event in hours and minutes. The duration is specified in standard ISO9601 format

Examples:

  • An interval of one hour: PT1H
  • An interval of one hour and 30 minutes: PT1H30M
  • An interval of 20 minutes: PT20M

Start frequency (step)

Regardless of the duration of the services, we may want them to start at a certain time.

The step field allows you to configure how often time slots are generated in which the event can be booked.

Example: Duration 1h Step 30m

{
duration: 'PT1H',
step: 'PT30M',
}

If we create an event of 1h with a step of 30m the following slots will be generated:

  • 09:00 - 10:00
  • 09:30 - 10:30
  • 10:00 - 11:00
  • 10:30 - 11:30

Example: Duration 1h Step 20m

{
duration: 'PT1H',
step: 'PT20M',
}

If we create an event of 1h with a step of 20m the following slots will be generated:

  • 09:00 - 10:00
  • 09:20 - 10:20
  • 09:40 - 10:40
  • 10:00 - 11:00
  • 10:20 - 11:20
  • 10:40 - 11:40

Schedule (repeatingAvailability)

Legacy Field

The repeatingAvailability field is legacy and will be replaced by the availabilitySchedule field in future versions of the API.

We recommend using availabilitySchedule for all new implementations. See the iCal-Based Availability Schedule section below for the modern approach, which supports:

  • Cross-midnight availability
  • Multi-day events
  • One-off specific dates
  • Complex recurrence patterns
  • Multiple duration options
  • Per-window durations
  • Exception dates

Allows you to configure the hours in which slots will be generated regularly.

The schedule object consists of two main properties, the time zone and the weekly periodicity.

Example: Working days from 09:00 to 18:00

Legacy approach (repeatingAvailability):

{
timeZone: 'Europe/Madrid',
weekly: {
SUNDAY: [],
MONDAY: [{ start: '09:00', end: '18:00' }],
TUESDAY: [{ start: '09:00', end: '18:00' }],
WEDNESDAY: [{ start: '09:00', end: '18:00' }],
THURSDAY: [{ start: '09:00', end: '18:00' }],
FRIDAY: [{ start: '09:00', end: '18:00' }],
SATURDAY: [],
}
}

Recommended approach (availabilitySchedule):

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT1H"]
},
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "09:00",
"duration": "PT9H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
}
]
}
}
}

To specify breaks, for example the corresponding to lunch time, it is enough to add another interval to the corresponding day.

Example: Working days from 09:00 to 14:00 and from 16:00 to 18:00

Legacy approach (repeatingAvailability):

{
timeZone: 'Europe/Madrid',
weekly: {
SUNDAY: [],
MONDAY: [{ start: '09:00', end: '14:00' }, { start: '16:00', end: '18:00' }],
TUESDAY: [{ start: '09:00', end: '14:00' }, { start: '16:00', end: '18:00' }],
WEDNESDAY: [{ start: '09:00', end: '14:00' }, { start: '16:00', end: '18:00' }],
THURSDAY: [{ start: '09:00', end: '14:00' }, { start: '16:00', end: '18:00' }],
FRIDAY: [{ start: '09:00', end: '14:00' }, { start: '16:00', end: '18:00' }],
SATURDAY: [],
}
}

Recommended approach (availabilitySchedule):

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT1H"]
},
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "09:00",
"duration": "PT5H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
},
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "16:00",
"duration": "PT2H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
}
]
}
}
}

Advanced rules

Buffers between bookings

Sometimes we want to block a time span before and after the events. An example is a doctor who wants to have 10 minutes before each appointment to read the patient's data calmly or a beauty clinic that has to disinfect the cabins after each session.

  • beforeBuffer: Allows you to specify a time margin before the events in duration format.
  • afterBuffer Allows you to specify this time margin after the events in duration format.

Example: A service that has to have 10 minutes before and 20 minutes after each appointment.

{
// 10 minutes before
beforeBuffer: "PT10M",
// 20 minutes after.
afterBuffer: "PT20M",
}

Example: A service that has to have 10 minutes before and 20 minutes after each appointment.

{
// 10 minutes before
beforeBuffer: "PT10M",
// 20 minutes after.
afterBuffer: "PT20M",
}

Maximum and minimun booking notice

This option allows you to restrict how far in advance a service can be booked.

Example: A service that has to be booked 24h in advance and maximum in the next 7 days

{
// The service has to be booked 24h in advance
minBookingNotice: "PT24H",
// The service has to be booked maximum in the next 7 days
maxBookingNotice: "PT7D",
}

When this option is enabled, bookings outside the specified time range will not be allowed. This is useful to avoid last minute bookings or too far in advance.

Max number of bookings per slot

Allows you to configure the maximum number of bookings that can be made in each available slot.

Example: A psychologist who can only see one patient at a time:

{
// 1 concurrent booking per slot is allowed.
"maxConcurrentBookings": 1,
}

Example: A spinning class with 10 bikes:

{
// 10 concurrent bookings per slot are allowed.
"maxConcurrentBookings": 10,
}

Limit of bookings per time unit. (maxBookingsPerTimeUnit)

Allows to configure the maximum number of bookings that can be made in a given time period. It contains an array of limits formed by the following fields:

  • perHour: Maximum number of bookings per hour.
  • perDay: Maximum number of bookings per day.
  • perWeek: Maximum number of bookings per week.
  • perMonth: Maximum number of bookings per month.
Important

It is considered that an event belongs to the period if its start date or its end date is within the period. Time periods are calculated according to the time zone specified in the TimeZone field of the service. If this field is not configured, UTC is used.

Example: A service that allows a maximum of 10 bookings per hour:

maxBookingsPerTimeUnit: {
byHour: 10,
}

### Example: Maximum 3 bookings per day, 15 per week or 30 per month.

{
maxBookingsPerTimeUnit: {
byDay: 3,
byWeek: 15,
byMonth: 30,
}
}

Units

In certain types of bookings the maxConcurrentBookings field is not enough if we want to represent situations where in each booking a variable number of bookable objects can be specified.

Example: A show that has 100 seats and for each reservation a minimum of 1 seat and a maximum of 5 can be reserved:

{
"availableUnits": 20,
"minUnitsPerBooking": 5,
"maxUnitsPerBooking": 20,
}

iCal-Based Availability Schedule

Recommended

The availabilitySchedule field is the recommended way to define availability. It provides a powerful, flexible system using iCal (RFC 5545) internally, supporting advanced scheduling scenarios that weren't possible with the legacy repeatingAvailability field.

The iCal-based availability schedule system allows you to define complex availability patterns including:

  • Cross-midnight availability (e.g., bar open 10pm-2am)
  • Multi-day events (e.g., weekend workshops)
  • One-off specific dates (e.g., special holiday hours)
  • Complex recurrence patterns (e.g., first Monday of each month)
  • Multiple duration options (e.g., offer 30min, 1h, or 2h sessions)
  • Per-window durations (e.g., 2h workshops on Mondays, 30min calls on Fridays)
  • Exception dates (e.g., exclude holidays)

Core Concepts

Availability Windows

An availability window defines a time period when bookings are possible. There are two types:

  1. Recurring Windows: Repeat on a schedule (daily, weekly, monthly, yearly)

    • Defined by a timezone, start time, duration, and recurrence pattern
    • Example: "Every Monday-Friday, 9am-5pm in America/New_York"
  2. One-Off Windows: Specific date/time ranges

    • Defined by explicit start and end datetimes
    • Example: "December 25, 2024, 10am-2pm UTC"

Duration Options

Each window (or the entire schedule) can specify what durations are allowed:

  1. Discrete Durations: A fixed list of allowed durations

    • Example: ["PT30M", "PT1H"] - only 30 minutes or 1 hour
  2. Range Durations: Any duration within a range with a step

    • Example: min: "PT1H", max: "PT4H", step: "PT15M" - any duration from 1h to 4h in 15-minute increments
  3. Fit to Window: The duration must exactly match the window size

    • Example: A 3-hour window only allows 3-hour bookings

Recurrence Patterns

Recurring windows use recurrence rules to define when they repeat:

  • Frequency: DAILY, WEEKLY, MONTHLY, or YEARLY
  • Interval: Repeat every N periods (e.g., every 2 weeks)
  • Days of Week: For weekly patterns (e.g., Monday, Wednesday, Friday)
  • Days of Month: For monthly patterns (e.g., 1st and 15th)
  • Months of Year: For yearly patterns
  • Until/Count: Optional end date or maximum occurrences

Basic Structure

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT30M", "PT1H"]
},
"slotStep": "PT30M",
"windows": [
{
"type": "recurring",
"timeZone": "America/New_York",
"startTime": "09:00",
"duration": "PT8H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
}
],
"exceptions": []
}
}
}

Duration Options Explained

Discrete Durations

Use when you want to offer specific, fixed durations:

{
"defaultDurations": {
"type": "discrete",
"values": ["PT30M", "PT1H", "PT2H"]
}
}

This allows bookings of exactly 30 minutes, 1 hour, or 2 hours.

Range Durations

Use when you want flexibility within a range:

{
"defaultDurations": {
"type": "range",
"min": "PT1H",
"max": "PT4H",
"step": "PT15M"
}
}

This allows any duration from 1 hour to 4 hours, in 15-minute increments (1h, 1h15m, 1h30m, ..., 4h).

Fit to Window

Use when the booking must fill the entire window:

{
"durations": {
"type": "fit"
}
}

This is typically used for one-off windows where the duration must match the window exactly.

Use Case Examples

Example 1: Standard Business Hours

Scenario: Available Monday-Friday 9am-5pm for 30min or 1h meetings

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT30M", "PT1H"]
},
"windows": [
{
"type": "recurring",
"timeZone": "America/New_York",
"startTime": "09:00",
"duration": "PT8H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
}
]
}
}
}

Availability Response (GET /v1/event-types/{id}/availability):

{
"timeSlots": [
{
"start": "2024-01-15T14:00:00Z",
"end": "2024-01-15T14:30:00Z",
"score": 1
},
{
"start": "2024-01-15T14:00:00Z",
"end": "2024-01-15T15:00:00Z",
"score": 1
},
{
"start": "2024-01-15T14:30:00Z",
"end": "2024-01-15T15:00:00Z",
"score": 1
}
],
"durationOptions": {
"type": "discrete",
"values": ["PT30M", "PT1H"]
}
}

Example 2: Cross-Midnight Availability

Scenario: Available Friday and Saturday 10pm-2am for 2h sessions that span midnight

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT2H"]
},
"slotStep": "PT1H",
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "22:00",
"duration": "PT4H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": ["FRIDAY", "SATURDAY"]
}
}
]
}
}
}

This creates slots like:

  • Friday 22:00-00:00 (spans midnight)
  • Friday 23:00-01:00 (spans midnight)
  • Saturday 22:00-00:00 (spans midnight)
  • Saturday 23:00-01:00 (spans midnight)

Availability Response (with ?duration=PT2H):

{
"timeSlots": [
{
"start": "2024-01-19T22:00:00+01:00",
"end": "2024-01-20T00:00:00+01:00",
"score": 1
},
{
"start": "2024-01-19T23:00:00+01:00",
"end": "2024-01-20T01:00:00+01:00",
"score": 1
}
],
"durationOptions": {
"type": "discrete",
"values": ["PT2H"]
},
"selectedDuration": "PT2H"
}

Example 3: Multi-Day Event

Scenario: Weekend workshop Friday 6pm through Sunday 3pm, 3h sessions only

{
"availabilitySchedule": {
"schedule": {
"windows": [
{
"type": "oneOff",
"start": {
"dateTime": "2024-03-15T18:00:00+00:00",
"timeZone": "Europe/London"
},
"end": {
"dateTime": "2024-03-17T15:00:00+00:00",
"timeZone": "Europe/London"
},
"durations": {
"type": "discrete",
"values": ["PT3H"]
}
}
]
}
}
}

Availability Response (with ?duration=PT3H):

{
"timeSlots": [
{
"start": "2024-03-15T18:00:00Z",
"end": "2024-03-15T21:00:00Z",
"score": 1
},
{
"start": "2024-03-15T21:00:00Z",
"end": "2024-03-16T00:00:00Z",
"score": 1
},
{
"start": "2024-03-16T09:00:00Z",
"end": "2024-03-16T12:00:00Z",
"score": 1
}
],
"durationOptions": {
"type": "discrete",
"values": ["PT3H"]
},
"selectedDuration": "PT3H"
}

Example 4: One-Off Specific Dates

Scenario: Special availability on specific dates only, 10am-2pm

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT2H"]
},
"windows": [
{
"type": "oneOff",
"start": {
"dateTime": "2024-12-25T10:00:00Z"
},
"end": {
"dateTime": "2024-12-25T14:00:00Z"
}
},
{
"type": "oneOff",
"start": {
"dateTime": "2024-12-26T10:00:00Z"
},
"end": {
"dateTime": "2024-12-26T14:00:00Z"
}
}
]
}
}
}

Availability Response:

{
"timeSlots": [
{
"start": "2024-12-25T10:00:00Z",
"end": "2024-12-25T12:00:00Z",
"score": 1
},
{
"start": "2024-12-25T12:00:00Z",
"end": "2024-12-25T14:00:00Z",
"score": 1
},
{
"start": "2024-12-26T10:00:00Z",
"end": "2024-12-26T12:00:00Z",
"score": 1
}
],
"durationOptions": {
"type": "discrete",
"values": ["PT2H"]
}
}

Example 5: Mixed Schedule with Per-Window Durations

Scenario:

  • Regular: Mon-Fri 10am-6pm, 30min to 2h in 30min increments
  • Special Dec 15: 12pm-2pm, exactly 2h (fit to window)
  • Special Dec 16: 3pm-6pm, exactly 3h
{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "range",
"min": "PT30M",
"max": "PT2H",
"step": "PT30M"
},
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Madrid",
"startTime": "10:00",
"duration": "PT8H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": [
"MONDAY",
"TUESDAY",
"WEDNESDAY",
"THURSDAY",
"FRIDAY"
]
}
},
{
"type": "oneOff",
"start": {
"dateTime": "2024-12-15T12:00:00+01:00",
"timeZone": "Europe/Madrid"
},
"end": {
"dateTime": "2024-12-15T14:00:00+01:00",
"timeZone": "Europe/Madrid"
},
"durations": {
"type": "fit"
}
},
{
"type": "oneOff",
"start": {
"dateTime": "2024-12-16T15:00:00+01:00",
"timeZone": "Europe/Madrid"
},
"end": {
"dateTime": "2024-12-16T18:00:00+01:00",
"timeZone": "Europe/Madrid"
},
"durations": {
"type": "discrete",
"values": ["PT3H"]
}
}
],
"exceptions": ["2024-12-25", "2024-12-26"]
}
}
}

Availability Response (default, no duration filter):

{
"timeSlots": [
{
"start": "2024-12-15T10:00:00+01:00",
"end": "2024-12-15T10:30:00+01:00",
"score": 1
},
{
"start": "2024-12-15T10:00:00+01:00",
"end": "2024-12-15T11:00:00+01:00",
"score": 1
},
{
"start": "2024-12-15T12:00:00+01:00",
"end": "2024-12-15T14:00:00+01:00",
"score": 1
}
],
"durationOptions": {
"type": "range",
"min": "PT30M",
"max": "PT2H",
"step": "PT30M"
}
}

Availability Response (with ?duration=PT3H):

{
"timeSlots": [
{
"start": "2024-12-16T15:00:00+01:00",
"end": "2024-12-16T18:00:00+01:00",
"score": 1
}
],
"durationOptions": {
"type": "range",
"min": "PT30M",
"max": "PT2H",
"step": "PT30M"
},
"selectedDuration": "PT3H"
}

Example 6: Complex Recurrence (Monthly Pattern)

Scenario: Available on 1st and 15th of every month, 2pm-5pm

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT1H", "PT2H"]
},
"windows": [
{
"type": "recurring",
"timeZone": "America/Los_Angeles",
"startTime": "14:00",
"duration": "PT3H",
"recurrence": {
"frequency": "MONTHLY",
"daysOfMonth": [1, 15]
}
}
]
}
}
}

Availability Response:

{
"timeSlots": [
{
"start": "2024-02-01T14:00:00-08:00",
"end": "2024-02-01T15:00:00-08:00",
"score": 1
},
{
"start": "2024-02-01T14:00:00-08:00",
"end": "2024-02-01T16:00:00-08:00",
"score": 1
},
{
"start": "2024-02-15T14:00:00-08:00",
"end": "2024-02-15T15:00:00-08:00",
"score": 1
}
],
"durationOptions": {
"type": "discrete",
"values": ["PT1H", "PT2H"]
}
}

Example 7: Flexible Duration Range

Scenario: Allow any duration from 1h to 4h in 15min increments

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "range",
"min": "PT1H",
"max": "PT4H",
"step": "PT15M"
},
"windows": [
{
"type": "recurring",
"timeZone": "Europe/Berlin",
"startTime": "09:00",
"duration": "PT9H",
"recurrence": {
"frequency": "DAILY"
}
}
]
}
}
}

Availability Response (with ?duration=PT2H30M):

{
"timeSlots": [
{
"start": "2024-01-15T09:00:00+01:00",
"end": "2024-01-15T11:30:00+01:00",
"score": 1
},
{
"start": "2024-01-15T09:15:00+01:00",
"end": "2024-01-15T11:45:00+01:00",
"score": 1
}
],
"durationOptions": {
"type": "range",
"min": "PT1H",
"max": "PT4H",
"step": "PT15M"
},
"selectedDuration": "PT2H30M"
}

Example 8: Daily Recurrence with Interval

Scenario: Available every other day, 10am-4pm

{
"availabilitySchedule": {
"schedule": {
"defaultDurations": {
"type": "discrete",
"values": ["PT1H", "PT2H"]
},
"windows": [
{
"type": "recurring",
"timeZone": "UTC",
"startTime": "10:00",
"duration": "PT6H",
"recurrence": {
"frequency": "DAILY",
"interval": 2
}
}
]
}
}
}

This creates availability on alternating days (e.g., Jan 1, Jan 3, Jan 5, ...).

Querying Availability

When querying the availability endpoint, you can filter by duration:

GET /v1/event-types/{id}/availability?from=2024-01-15&days=7&duration=PT1H

Query Parameters:

  • from: Start date (YYYY-MM-DD)
  • days: Number of days to search (default: 7)
  • duration: (Optional) Filter slots by this specific duration. Must match one of the available durationOptions.

Response includes:

  • timeSlots: Array of available time slots
  • durationOptions: Available duration options for this event type (null for legacy schedules)
  • selectedDuration: The duration that was used to filter slots (only present when duration query param was provided)

Slot Step

The slotStep field controls how frequently slots are generated within a window. If not specified, it defaults to the smallest available duration.

Example: With a window from 9am-5pm and slotStep: "PT30M", slots will be generated every 30 minutes:

  • 09:00, 09:30, 10:00, 10:30, ...

If slotStep is not provided and you have discrete durations ["PT30M", "PT1H"], it will default to PT30M (the smallest).

Exception Dates

You can exclude specific dates from recurring windows:

{
"schedule": {
"windows": [
{
"type": "recurring",
"timeZone": "America/New_York",
"startTime": "09:00",
"duration": "PT8H",
"recurrence": {
"frequency": "WEEKLY",
"daysOfWeek": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]
}
}
],
"exceptions": ["2024-12-25", "2024-12-26", "2025-01-01"]
}
}

Exception dates are interpreted in the timezone of each window. For multi-timezone schedules, an exception date will exclude that date in all timezones.

Migration from Legacy System

The legacy repeatingAvailability system continues to work for backward compatibility. However, we recommend migrating to availabilitySchedule for all new implementations. The new availabilitySchedule system takes precedence when both are present.

Key Differences:

FeatureLegacy (repeatingAvailability)New (availabilitySchedule)
Cross-midnight❌ Not supported✅ Supported
Multi-day events❌ Not supported✅ Supported
One-off dates❌ Not supported✅ Supported
Complex recurrence❌ Weekly only✅ Daily, Weekly, Monthly, Yearly
Multiple durations❌ Single fixed duration✅ Discrete, Range, or Fit
Per-window durations❌ Not supported✅ Supported
Exception dates❌ Not supported✅ Supported

API Reference

Field Structure:

interface AvailabilitySchedule {
schedule?: ScheduleDefinition;
ical?: string; // Raw iCal format (alternative to schedule)
}

interface ScheduleDefinition {
defaultDurations?: DurationOptions;
slotStep?: string; // ISO 8601 duration
windows: AvailabilityWindow[];
exceptions?: string[]; // Dates in YYYY-MM-DD format
}

type AvailabilityWindow = RecurringWindow | OneOffWindow;

interface RecurringWindow {
type: "recurring";
timeZone: string; // IANA timezone (e.g., "America/New_York")
startTime: string; // HH:MM format
duration: string; // ISO 8601 duration (supports cross-midnight)
recurrence: Recurrence;
durations?: DurationOptions; // Optional per-window override
}

interface OneOffWindow {
type: "oneOff";
start: ZonedDateTime;
end: ZonedDateTime;
durations?: DurationOptions; // Optional per-window override
}

interface ZonedDateTime {
dateTime: string; // ISO 8601 with offset (e.g., "2024-03-15T18:00:00+01:00")
timeZone?: string; // Optional IANA timezone
}

interface Recurrence {
frequency: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
interval?: number; // Every N periods (default: 1)
daysOfWeek?: DayOfWeek[]; // For WEEKLY
daysOfMonth?: number[]; // For MONTHLY (1-31)
monthsOfYear?: number[]; // For YEARLY (1-12)
until?: string; // End date (YYYY-MM-DD)
count?: number; // Max occurrences
}

type DurationOptions =
| { type: "discrete"; values: string[] } // ISO 8601 durations
| { type: "range"; min: string; max?: string; step: string }
| { type: "fit" };

type DayOfWeek =
| "MONDAY"
| "TUESDAY"
| "WEDNESDAY"
| "THURSDAY"
| "FRIDAY"
| "SATURDAY"
| "SUNDAY";

Validation Rules:

  1. Either defaultDurations must be provided, OR every window must have its own durations field
  2. For range durations, if max is not provided, it will be derived from the window size
  3. fit durations can only be used with windows that have a fixed size
  4. Discrete duration values must not exceed the window duration
  5. daysOfWeek should only be used with WEEKLY frequency
  6. daysOfMonth should only be used with MONTHLY or YEARLY frequency
  7. monthsOfYear should only be used with YEARLY frequency