LogoRecal
JS/TS SDK

Scheduling

Find optimal time slots for meetings and appointments.

Understanding Scheduling

The Scheduling API is your intelligent assistant for finding optimal meeting times and managing availability across users and organizations. It analyzes calendar data, applies business rules, and returns ready-to-book time slots, eliminating the back-and-forth of manual scheduling coordination.

Key Concepts

Smart Slot Generation: Automatically finds available time periods based on calendar data, working hours, and your preferences.

Multi-Calendar Awareness: Considers calendars across all connected providers (Google, Microsoft, etc.) to prevent conflicts and double-booking.

Business Rules Integration: Applies working hours, padding between meetings, and custom schedules to match real-world constraints.

Team Coordination: Finds times when multiple people or entire organizations are available simultaneously.

Timezone Intelligence: Handles timezone conversions and displays results in the appropriate timezone for each participant.


Single User Scheduling

Basic User Availability

Find available time slots for a single user with simple constraints.

// Find available time slots (minimal config)
const slots = await recal.scheduling.getSlots('user_id', {
    start: '2024-01-15T00:00:00Z',
    end: '2024-01-20T23:59:59Z',
    slotDuration: '30'  // Only required: slot duration in minutes
})

// Or with more options
const detailedSlots = await recal.scheduling.getSlots('user_id', {
    start: '2024-01-15T00:00:00Z',
    end: '2024-01-20T23:59:59Z',
    slotDuration: '30',  // Duration of each slot in minutes
    padding: '10',  // Padding between slots (minutes)
    maxOverlaps: '0',  // Only completely free slots
    earliestTimeEachDay: '09:00',  // Format: HH:mm
    latestTimeEachDay: '17:00',  // Format: HH:mm
    provider: 'google',  // optional: filter by provider
    timeZone: 'America/New_York'  // optional
})

Parameters:

  • start (required): ISO 8601 timestamp for range start
  • end (required): ISO 8601 timestamp for range end
  • slotDuration (required): Duration of each slot in minutes (as string)
  • padding (optional): Minutes between meetings (as string, default: '0')
  • maxOverlaps (optional): Allow slots with up to N overlapping events (as string, default: '0')
  • earliestTimeEachDay (optional): Start of working hours (HH:mm format)
  • latestTimeEachDay (optional): End of working hours (HH:mm format)
  • provider (optional): Filter by 'google' or 'microsoft'
  • timeZone (optional): Timezone for results

Advanced User Availability

For more complex scheduling requirements with custom work schedules per day and advanced parameters.

// Define custom schedules
const slots = await recal.scheduling.getAdvancedSlots(
    'user_id',
    {
        start: '2024-01-15T00:00:00Z',
        end: '2024-01-20T23:59:59Z',
        slotDuration: '60',
        padding: '15',
        maxOverlaps: '1'  // Allow up to 1 overlapping event
    },
    {
        schedules: [
            {
                days: ['monday', 'wednesday', 'friday'],
                start: '09:00',
                end: '17:00'
            },
            {
                days: ['tuesday', 'thursday'],
                start: '10:00',
                end: '16:00'
            }
        ]
    }
)

Query Parameters:

  • start (required): ISO 8601 timestamp for range start
  • end (required): ISO 8601 timestamp for range end
  • slotDuration (required): Duration of each slot in minutes (as string)
  • padding (optional): Minutes between meetings (as string, default: '0')
  • maxOverlaps (optional): Allow slots with up to N overlapping events (as string, default: '0')
  • provider (optional): Filter by 'google' or 'microsoft'
  • timeZone (optional): Timezone for results

Schedule Options:

  • days: Array of day names ('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday')
  • start: Working hours start time (HH:mm format)
  • end: Working hours end time (HH:mm format)

Note: The maxOverlaps parameter is useful for scenarios where some event conflicts are acceptable (e.g., optional meetings, tentative events). When set to 0 (default), only completely free slots are returned.


Multi-User Scheduling

Find available time slots for multiple users independently. This endpoint processes each user separately and returns individual availability results.

Important: This is NOT a "find common availability" endpoint. Each user's available slots are calculated independently based on their own calendars and schedules. If you need to find times when ALL users are available simultaneously, you'll need to intersect the results yourself.

Need a built-in common availability finder or other scheduling features? If you think a native "find common slots" feature or any other scheduling functionality would be useful for your use case, we'd love to hear from you! Please reach out to hello@recal.dev and explain your specific requirements.

// Basic multi-user availability
const results = await recal.scheduling.getMultiUserSlots(
    {
        start: '2024-01-15T00:00:00Z',
        end: '2024-01-19T23:59:59Z',
        slotDuration: '60'
    },
    {
        users: [
            { id: 'user_1' },
            { id: 'user_2' },
            { id: 'user_3' }
        ]
    }
)

// With custom schedules and calendars per user
const advancedResults = await recal.scheduling.getMultiUserSlots(
    {
        start: '2024-01-15T00:00:00Z',
        end: '2024-01-19T23:59:59Z',
        slotDuration: '60',
        padding: '15',
        maxOverlaps: '1',  // Allow up to 1 overlapping event per user
        provider: 'google',  // Filter to specific provider
        timeZone: 'America/New_York'
    },
    {
        users: [
            {
                id: 'user_1',
                calendarIds: ['primary', 'work@company.com'],  // Specific calendars
                schedules: [
                    {
                        days: ['monday', 'wednesday', 'friday'],
                        start: '09:00',
                        end: '17:00'
                    }
                ]
            },
            {
                id: 'user_2',
                schedules: [
                    {
                        days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
                        start: '10:00',
                        end: '16:00'
                    }
                ]
            },
            { id: 'user_3' }  // Uses default constraints
        ]
    }
)

Query Parameters:

  • start (required): ISO 8601 timestamp for range start
  • end (required): ISO 8601 timestamp for range end
  • slotDuration (required): Duration of each slot in minutes (as string)
  • padding (optional): Minutes between meetings (as string, default: '0')
  • maxOverlaps (optional): Allow slots with up to N overlapping events (as string, default: '0')
  • provider (optional): Filter by 'google' or 'microsoft'
  • timeZone (optional): Timezone for results

User Configuration:

  • id (required): User ID
  • calendarIds (optional): Calendar ID, or array of IDs, to check (defaults to ['primary'])
  • schedules (optional): Array of custom schedule rules for this user

Response Format:

The endpoint returns an array with one entry per user. Users are resolved independently, so one user failing never fails the request — each entry is discriminated by status.

Success Response (status: 'ok'):

{
    userId: string,              // The user ID you provided
    status: 'ok',
    availableSlots: Array<{      // Available time slots for this user
        start: Date,             // Date object, converted from ISO 8601
        end: Date
    }>,
    options: {                   // The scheduling parameters used
        padding: number,
        slotDuration: number,
        start: Date,
        end: Date,
        schedules?: Array<...>,
        maxOverlaps: number,
        calendarIds?: string | string[]
    },
    failedCalendars: Array<{     // Since 1.2.0 — always present, [] when nothing failed
        calendarId: string,
        provider: 'google' | 'microsoft',
        reason: FreeBusyFailureReason,
        message: string
    }>
}

Error Response (status: 'error'):

{
    userId: string,              // The user ID you provided
    status: 'error',
    error: string,               // Error message (e.g., "User not found", "User has no connected calendars")
    reason?: FreeBusyFailureReason  // Since 1.2.0 — present only when a calendar provider failed
}

The two provisioning messages — "User not found" and "User has no connected calendars" — carry no reason; they are states you control rather than failures. Every other error entry carries one, and its error text is that reason's fixed message. See Troubleshooting for the ten codes.

A slot beside a non-empty `failedCalendars` may not be free

Slots are computed from the busy time Recal could read, so a calendar it could not read cannot narrow them. If the conflict lived on that calendar, the slot still looks open — availability errs wide, never narrow. Decide per request whether to book, confirm out of band, or withhold: what a missing calendar does to availability.

`failedCalendars` needs `maxOverlaps: 0`

With maxOverlaps above 0, a calendar that cannot be read fails that user's whole entry rather than trimming it — you get a status: 'error' entry instead of an ok one with failedCalendars. Changing only maxOverlaps therefore changes which entry kind a bad calendar produces.

Narrow on `status`

Check status before reading availableSlots — it is absent on failed entries. An entry never reports an empty availableSlots to mean failure — but check failedCalendars too: [] means no availability among the calendars that could be read.

Example Response:

[
    {
        userId: 'user_1',
        status: 'ok',
        availableSlots: [
            { start: '2024-01-15T09:00:00Z', end: '2024-01-15T10:00:00Z' },
            { start: '2024-01-15T14:00:00Z', end: '2024-01-15T15:00:00Z' }
        ],
        options: {
            padding: 15,
            slotDuration: 60,
            start: '2024-01-15T00:00:00.000Z',
            end: '2024-01-19T23:59:59.000Z',
            maxOverlaps: 0
        },
        failedCalendars: []
    },
    {
        userId: 'user_2',
        status: 'ok',
        availableSlots: [
            { start: '2024-01-15T10:00:00Z', end: '2024-01-15T11:00:00Z' }
        ],
        options: { /* ... */ },
        failedCalendars: [
            {
                calendarId: 'old-team@group.calendar.google.com',
                provider: 'google',
                reason: 'notFound',
                message: 'The calendar does not exist or is no longer subscribed'
            }
        ]
    },
    {
        userId: 'user_3',
        status: 'error',
        error: 'User has no connected calendars'
    },
    {
        userId: 'user_4',
        status: 'error',
        error: 'The stored calendar authorization is no longer valid',
        reason: 'authorizationInvalid'
    }
]

Key Behaviors:

  • Each user is processed independently and concurrently
  • If one user fails, others still succeed (error isolation)
  • Users without OAuth connections return an error response
  • Non-existent users return "User not found" error
  • Empty availableSlots with empty failedCalendars means the user is completely booked; with a non-empty failedCalendars it means booked among what could be read

Use Cases:

  • Displaying individual availability for multiple team members
  • Building a team scheduling UI showing each person's free slots
  • Batch processing availability checks
  • Comparing schedules across team members

Organization-Wide Scheduling

For team-wide availability, you can also use organization-level scheduling methods:

// Find organization-wide available time slots
const { data: orgSlots, failedUsers } = await recal.organizations.getScheduling('org-slug', {
    start: '2024-01-15T00:00:00Z',
    end: '2024-01-20T23:59:59Z',
    slotDuration: '60',
    padding: '15',
    maxOverlaps: '0',  // Only completely free slots
    earliestTimeEachDay: '09:00',
    latestTimeEachDay: '17:00',
    provider: ['google', 'microsoft'],
    timeZone: 'America/New_York'
})

Parameters:

  • start (required): ISO 8601 timestamp for range start
  • end (required): ISO 8601 timestamp for range end
  • slotDuration (required): Duration of each slot in minutes (as string)
  • padding (optional): Minutes between meetings (as string, default: '0')
  • maxOverlaps (optional): Allow slots with up to N overlapping events (as string, default: '0')
  • earliestTimeEachDay (optional): Start of working hours (HH:mm format)
  • latestTimeEachDay (optional): End of working hours (HH:mm format)
  • provider (optional): Filter by provider(s)
  • timeZone (optional): Timezone for results

Since 1.2.0 this method resolves to { data, failedUsers }. A non-empty failedUsers means the slots were computed without those members' calendars — see Troubleshooting.

See Organizations for more details on team scheduling.


Response Format

Single User Methods

Both getSlots() and getAdvancedSlots() return an object with available slots and the options used:

{
    availableSlots: [
        {
            start: '2024-01-15T10:00:00Z',
            end: '2024-01-15T10:30:00Z'
        },
        {
            start: '2024-01-15T14:00:00Z',
            end: '2024-01-15T14:30:00Z'
        },
        {
            start: '2024-01-16T09:00:00Z',
            end: '2024-01-16T09:30:00Z'
        }
    ],
    options: {
        padding: 15,
        slotDuration: 30,
        start: '2024-01-15T00:00:00.000Z',
        end: '2024-01-20T23:59:59.000Z',
        maxOverlaps: 0
    }
}

Multi-User Methods

getMultiUserSlots() returns an array with one entry per user - see the detailed response format in the Multi-User Scheduling section above.


Use Cases

Simple Booking System

async function findNextAvailableSlot(userId: string) {
    const now = new Date()
    const oneWeekLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)

    const result = await recal.scheduling.getSlots(userId, {
        start: now.toISOString(),
        end: oneWeekLater.toISOString(),
        slotDuration: '30',
        padding: '10',
        earliestTimeEachDay: '09:00',
        latestTimeEachDay: '17:00'
    })

    return result.availableSlots[0]  // Return first available slot
}

Team Availability Dashboard

async function getTeamAvailability(userIds: string[]) {
    const startDate = new Date()
    const endDate = new Date(startDate.getTime() + 14 * 24 * 60 * 60 * 1000)

    const results = await recal.scheduling.getMultiUserSlots(
        {
            start: startDate.toISOString(),
            end: endDate.toISOString(),
            slotDuration: '60',
            padding: '15'
        },
        {
            users: userIds.map(id => ({ id }))
        }
    )

    // Process results for each user
    return results.map(result => {
        if ('error' in result) {
            return {
                userId: result.userId,
                error: result.error,
                slots: []
            }
        }
        return {
            userId: result.userId,
            slots: result.availableSlots.slice(0, 5)  // First 5 slots per user
        }
    })
}

// Find common availability across all users
async function findCommonAvailability(userIds: string[]) {
    const startDate = new Date()
    const endDate = new Date(startDate.getTime() + 7 * 24 * 60 * 60 * 1000)

    const results = await recal.scheduling.getMultiUserSlots(
        {
            start: startDate.toISOString(),
            end: endDate.toISOString(),
            slotDuration: '60',
            padding: '15'
        },
        {
            users: userIds.map(id => ({ id }))
        }
    )

    // Filter out errors and get only successful results
    const successResults = results.filter(r => 'availableSlots' in r)

    if (successResults.length === 0) {
        return []
    }

    // Find slots that appear in ALL users' availability
    const firstUserSlots = successResults[0].availableSlots
    return firstUserSlots.filter(slot =>
        successResults.every(result =>
            result.availableSlots.some(s =>
                s.start === slot.start && s.end === slot.end
            )
        )
    )
}

Custom Work Schedule

async function findSlotsWithFlexibleHours(userId: string) {
    const result = await recal.scheduling.getAdvancedSlots(
        userId,
        {
            start: '2024-01-15T00:00:00Z',
            end: '2024-01-20T23:59:59Z',
            slotDuration: '45',
            padding: '15'
        },
        {
            schedules: [
                {
                    days: ['monday', 'wednesday', 'friday'],
                    start: '08:00',
                    end: '12:00'
                },
                {
                    days: ['tuesday', 'thursday'],
                    start: '13:00',
                    end: '18:00'
                }
            ]
        }
    )

    return result.availableSlots
}

Best Practices

  1. Appropriate Date Ranges: Query 1-2 weeks at a time for optimal performance
  2. Reasonable Slot Durations: Use 15, 30, or 60-minute slots for best results
  3. Include Padding: Add 5-15 minutes between meetings for buffer time
  4. Set Working Hours: Use earliestTimeEachDay and latestTimeEachDay to respect work-life balance
  5. Timezone Awareness: Always specify timezone when displaying slots to users
  6. Cache Results: Slot availability is relatively stable - cache for 5-15 minutes
  7. Limit User Count: For multi-user scheduling, optimal performance with 2-10 users
  8. Use Custom Schedules: For complex availability patterns, use getAdvancedSlots() or per-user schedules

On this page