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 startend(required): ISO 8601 timestamp for range endslotDuration(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 startend(required): ISO 8601 timestamp for range endslotDuration(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 startend(required): ISO 8601 timestamp for range endslotDuration(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 IDcalendarIds(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
availableSlotswith emptyfailedCalendarsmeans the user is completely booked; with a non-emptyfailedCalendarsit 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 startend(required): ISO 8601 timestamp for range endslotDuration(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
- Appropriate Date Ranges: Query 1-2 weeks at a time for optimal performance
- Reasonable Slot Durations: Use 15, 30, or 60-minute slots for best results
- Include Padding: Add 5-15 minutes between meetings for buffer time
- Set Working Hours: Use
earliestTimeEachDayandlatestTimeEachDayto respect work-life balance - Timezone Awareness: Always specify timezone when displaying slots to users
- Cache Results: Slot availability is relatively stable - cache for 5-15 minutes
- Limit User Count: For multi-user scheduling, optimal performance with 2-10 users
- Use Custom Schedules: For complex availability patterns, use
getAdvancedSlots()or per-user schedules
Related
- See Calendar & Availability for busy time queries
- See Organizations for team-wide scheduling
- See Events for booking the scheduled slots
- See Troubleshooting for partial answers and what each
reasonmeans