
Security News
Open VSX Begins Implementing Pre-Publish Security Checks After Repeated Supply Chain Incidents
Following multiple malicious extension incidents, Open VSX outlines new safeguards designed to catch risky uploads earlier.
A powerful, type-safe SDK for interacting with the Recal calendar API. Build sophisticated calendar integrations with support for Google and Microsoft calendar providers.
# Using npm
npm install recal-sdk
# Using yarn
yarn add recal-sdk
# Using bun
bun add recal-sdk
import { RecalClient } from 'recal-sdk'
// Initialize the client with token from .env file (RECAL_TOKEN)
const recal = new RecalClient()
// Or manually provide the token
const recal = new RecalClient({
token: "recal_xyz"
})
Security Note: This SDK is designed for server-side use. Never expose your API token in client-side code.
The SDK is organized into logical service modules:
calendar - Event management and busy queriesscheduling - Availability and booking managementusers - User profile and settingsorganizations - Team and organization managementoauth - Calendar provider authenticationAll date/time operations support timezone specification via the timeZone parameter.
TypeScript note
- Use the exported
Providerenum for provider arguments.- Date/time fields in responses are parsed into
Dateobjects at runtime.import { Provider } from 'recal-sdk'
// Get user's availability (simplest form)
const busy = await recal.calendar.getBusy(
'user_id',
new Date('2024-01-01'),
new Date('2024-01-07')
)
// Or with optional filters
const busyFiltered = await recal.calendar.getBusy(
'user_id',
new Date('2024-01-01'),
new Date('2024-01-07'),
{
provider: 'google', // optional: filter by provider
calendarIds: ["calendar_id"] // optional: filter by calendarIds
timeZone: 'America/New_York', // optional: timezone
}
)
// Get all events in a date range (simplest form)
const events = await recal.calendar.getEvents(
'user_id',
new Date('2024-01-01'),
new Date('2024-01-31')
)
// Or with optional filters
const eventsFiltered = await recal.calendar.getEvents(
'user_id',
new Date('2024-01-01'),
new Date('2024-01-31'),
{
provider: 'google', // optional: filter by provider
calendarIds: ["calendar_id"] // optional: filter by calendarIds
timeZone: 'Europe/London' // optional: timezone
}
)
// Create a new event (without optional timezone)
const event = await recal.calendar.createEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
event: {
subject: 'Team Meeting',
description: 'Weekly sync',
start: new Date('2024-01-15T10:00:00Z'),
end: new Date('2024-01-15T11:00:00Z'),
attendees: [
{ email: 'colleague@company.com' }
]
}
})
// Or with timezone option
const eventWithTZ = await recal.calendar.createEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
event: {
subject: 'Team Meeting',
description: 'Weekly sync',
start: new Date('2024-01-15T10:00:00Z'),
end: new Date('2024-01-15T11:00:00Z'),
attendees: [
{ email: 'colleague@company.com' }
]
},
options: { timeZone: 'Europe/Berlin' } // optional
})
// Get an existing event
const event = await recal.calendar.getEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
eventId: 'event_id',
options: { timeZone: 'Europe/Berlin' } // optional
})
// Update an existing event (simplest form)
const updated = await recal.calendar.updateEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
eventId: 'event_id',
event: {
subject: 'Updated Meeting Title',
start: new Date('2024-01-15T14:00:00Z'),
end: new Date('2024-01-15T15:00:00Z')
}
})
// or with more options
const updated = await recal.calendar.updateEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
eventId: 'event_id',
event: {
subject: 'Updated Meeting title',
description: 'Updated description',
start: new Date('2024-01-15T11:00:00Z'),
end: new Date('2024-01-15T12:00:00Z'),
attendees: [
{ email: 'colleague@company.com' }
]
},
options: { timeZone: 'Europe/Berlin' } // optional
})
// Delete an event
await recal.calendar.deleteEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
eventId: 'event_id'
})
Meta events allow you to work with events across multiple calendar providers:
// Create event across all connected calendars (default behavior)
const metaEvent = await recal.calendar.createEventByMetaId(
'user_id',
{
subject: 'Cross-platform meeting',
start: new Date('2024-01-20T15:00:00Z'),
end: new Date('2024-01-20T16:00:00Z')
}
)
// Or specify which providers and timezone to use
const metaEventSpecific = await recal.calendar.createEventByMetaId(
'user_id',
{
subject: 'Cross-platform meeting',
start: new Date('2024-01-20T15:00:00Z'),
end: new Date('2024-01-20T16:00:00Z')
},
{
provider: ['google', 'microsoft'], // Create on specific providers
timeZone: 'Europe/Berlin' // optional
}
)
// Get event across all connected calendars (default behavior)
const metaEventGet = await recal.calendar.getEventByMetaId(
'user_id',
metaEvent.metaId
)
// Update across all calendars using meta ID
await recal.calendar.updateEventByMetaId(
'user_id',
metaEvent.metaId,
{ subject: 'Updated title' }
)
// Delete from all calendars
await recal.calendar.deleteEventByMetaId(
'user_id',
metaEvent.metaId
)
// Find available time slots (minimal config)
const availability = await recal.scheduling.user(
'user_id',
new Date('2024-01-15'),
new Date('2024-01-20'),
{
slotDuration: 30 // Only required: slot duration in minutes
}
)
// Or with more options
const availabilityDetailed = await recal.scheduling.user(
'user_id',
new Date('2024-01-15'),
new Date('2024-01-20'),
{
slotDuration: 30, // Duration of each slot in minutes
padding: 0, // Padding between slots
earliestTimeEachDay: '09:00', // Format: HH:mm
latestTimeEachDay: '17:00', // Format: HH:mm
provider: 'google', // optional: filter by provider
timeZone: 'America/New_York' // optional
}
)
// Find availability even when user already has events scheduled
const availabilityWithOverlaps = await recal.scheduling.user(
'user_id',
new Date('2024-01-15'),
new Date('2024-01-20'),
{
slotDuration: 30,
maxOverlaps: 2, // Allow up to 2 overlapping events per slot
timeZone: 'America/New_York'
}
)
// maxOverlaps: 2, means 1 slot is still available even if the user has 2 existing events at the same time.
// This enables booking a n concurrent events - useful for optional meetings,
// tentative invites, or users who can handle multiple events simultaneous, like a team that has just one calendar.
// Find available time slots with custom schedules
const schedules = [
{
days: ['monday'], // Monday
start: '09:00',
end: '17:00'
},
// ... more schedule rules
]
// Minimal config
const availability = await recal.scheduling.userAdvanced(
'user_id',
schedules,
new Date('2024-01-15'),
new Date('2024-01-20'),
{ slotDuration: 30 } // Only required option
)
// Or with more options
const availabilityDetailed = await recal.scheduling.userAdvanced(
'user_id',
schedules,
new Date('2024-01-15'),
new Date('2024-01-20'),
{
slotDuration: 30,
padding: 15,
provider: 'google', // optional
timeZone: 'America/New_York' // optional
}
)
// Find organization-wide available time slots (minimal)
const orgAvailability = await recal.scheduling.organization(
'org-slug',
new Date('2024-01-15'),
new Date('2024-01-20'),
{ slotDuration: 60 } // Only required option
)
// Or with constraints
const orgAvailabilityConstrained = await recal.scheduling.organization(
'org-slug',
new Date('2024-01-15'),
new Date('2024-01-20'),
{
slotDuration: 60,
padding: 0,
earliestTimeEachDay: '09:00',
latestTimeEachDay: '17:00',
provider: ['google', 'microsoft'], // optional
timeZone: 'America/New_York' // optional
}
)
// Get user information (basic)
const user = await recal.users.get('user_id', {})
// Or with additional data
const userWithDetails = await recal.users.get('user_id', {
includeOrgs: true, // Include organizations
includeOAuth: true // Include OAuth connections
})
console.log(user.id)
// Get all users
const users = await recal.users.listAll()
// Create a new user (without organizations)
const user = await recal.users.create('user_id')
// Or with organization memberships
const userWithOrgs = await recal.users.create(
'user_id',
['org-slug-1', 'org-slug-2'] // optional: organization slugs
)
// Update user ID
const updatedUser = await recal.users.update('old_user_id', {
id: 'new_user_id'
})
// Delete a user
const deletedUser = await recal.users.delete('user_id')
// Get organization by slug
const org = await recal.organizations.get('acme-corp')
// Get all organizations
const orgs = await recal.organizations.listAll()
// Get organizations for a specific user
const userOrgs = await recal.organizations.listAllFromUser('user_id')
// Create a new organization
const org = await recal.organizations.create(
'acme-corp', // slug
'Acme Corporation' // name
)
// Update organization
const updated = await recal.organizations.update('acme-corp', {
slug: 'new-slug',
name: 'New Name'
})
// Get all members
const members = await recal.organizations.getMembers('acme-corp')
// Add members
await recal.organizations.addMembers(
'acme-corp',
['user_id_1', 'user_id_2']
)
// Remove members
await recal.organizations.removeMembers(
'acme-corp',
['user_id_1', 'user_id_2']
)
// Get team availability (simplest form)
const teamBusy = await recal.calendar.getOrgWideBusy(
'acme-corp',
new Date('2024-01-15'),
new Date('2024-01-20'),
true // primaryOnly: only check primary calendars
)
// Or with optional filters
const teamBusyFiltered = await recal.calendar.getOrgWideBusy(
'acme-corp',
new Date('2024-01-15'),
new Date('2024-01-20'),
true, // primaryOnly: only check primary calendars
{
provider: 'google', // optional: filter by provider
timeZone: 'America/New_York' // optional
}
)
// Get OAuth authorization URL (with defaults)
const link = await recal.oauth.getLink(
'user_id',
'google'
)
// Or with custom options
const linkWithOptions = await recal.oauth.getLink(
'user_id',
'google',
{
scope: ['edit'], // 'edit' or 'free-busy' (for OAuth scopes)
accessType: 'offline', // 'offline' or 'online'
redirectUrl: 'https://app.example.com/callback' // optional
}
)
console.log(link.url) // Use this URL to redirect user
// Get OAuth URLs for all providers (simplest)
const links = await recal.oauth.getBulkLinks('user_id')
// Or with specific providers and options
const linksFiltered = await recal.oauth.getBulkLinks(
'user_id',
{
provider: ['google', 'microsoft'],
scope: ['edit'],
accessType: 'offline'
}
)
// Get all OAuth connections for a user
const connections = await recal.oauth.getAllConnections(
'user_id',
true // redacted (default: true)
)
// Get specific provider connection
const googleConnection = await recal.oauth.getConnection(
'user_id',
'google',
false // redacted
)
// Set OAuth tokens manually
const connection = await recal.oauth.setConnection(
'user_id',
'google',
{
accessToken: 'access_token',
refreshToken: 'refresh_token', // optional
scope: ['edit'],
expiresAt: new Date('2024-12-31'), // optional
email: 'user@example.com' // optional
}
)
// Disconnect a provider
await recal.oauth.disconnect('user_id', 'google')
// Verify OAuth code from callback
const result = await recal.oauth.verify(
'google',
'auth_code_from_callback',
['edit'], // 'edit' or 'free-busy' - single scope, not array
'state_parameter',
'https://app.example.com/callback' // optional
)
The SDK provides specific error types for different scenarios:
import {
UserNotFoundError,
EventNotFoundError,
OAuthConnectionNotFoundError,
OrganizationNotFoundError
} from 'recal-sdk'
try {
const event = await recal.calendar.getEvent({
userId: 'user_id',
provider: 'google',
calendarId: 'calendar_id',
eventId: 'event_id'
})
} catch (error) {
if (error instanceof UserNotFoundError) {
console.log('User does not exist:', error.userId)
} else if (error instanceof EventNotFoundError) {
console.log('Event not found:', error.eventId)
} else if (error instanceof OAuthConnectionNotFoundError) {
console.log('Calendar not connected:', error.provider)
}
}
// Process multiple users' calendars (simplest form)
const userIds = ['user1', 'user2', 'user3']
const allEvents = await Promise.all(
userIds.map(userId =>
recal.calendar.getEvents(
userId,
new Date('2024-01-01'),
new Date('2024-01-31')
)
)
)
// Get all busy data (without filtering)
const startDate = new Date('2024-01-01')
const endDate = new Date('2024-01-31')
const allBusy = await recal.calendar.getBusy(
'user_id',
startDate,
endDate
)
// Or aggregate by specific providers
const providers: Provider[] = ['google', 'microsoft']
const busyTimes = await Promise.all(
providers.map(provider =>
recal.calendar.getBusy(
'user_id',
startDate,
endDate,
{ provider } // filter by specific provider
)
)
)
// Process the busy times as needed for your application
// Each element is Busy = TimeRange[]; flatten into a single array of TimeRange
const allBusyPeriods = busyTimes.flat()
// Use custom base URL
const recal = new RecalClient({
token: 'recal_token',
url: 'https://api.recal.dev' // optional, this is the default
})
// 1. Check availability
const availability = await recal.scheduling.user(
'consultant_id',
new Date(),
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Next 7 days
{
slotDuration: 60, // 60-minute slots
padding: 15, // 15-minute padding between slots
earliestTimeEachDay: '09:00',
latestTimeEachDay: '17:00',
timeZone: 'America/New_York'
}
)
// 2. Display available slots to user
const availableSlots = availability.availableSlots // Already filtered for availability
// 3. User selects a slot and provides their information
const selectedSlot = availableSlots[0] // Example: first available slot
const clientName = 'John Doe'
const clientEmail = 'john@example.com'
// 4. Create an event for the selected slot (using calendar service)
const booking = await recal.calendar.createEvent({
userId: 'consultant_id',
provider: 'google',
calendarId: 'primary',
event: {
subject: 'Consultation with ' + clientName,
description: 'Initial consultation',
start: selectedSlot.start,
end: selectedSlot.end,
attendees: [{ email: clientEmail }]
}
})
// 5. Send confirmation
console.log('Booking confirmed:', booking.id)
// Sync events between providers
async function syncCalendars(userId: string) {
// Get all events from all providers
const allEvents = await recal.calendar.getEvents(
userId,
new Date(),
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
)
// Or get events from Google only
const googleEvents = await recal.calendar.getEvents(
userId,
new Date(),
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
{ provider: 'google' }
)
// Copy to Microsoft calendar
for (const event of googleEvents) {
if (!event.metaId) { // Not already synced
await recal.calendar.createEvent({
userId,
provider: 'microsoft',
calendarId: 'primary',
event: {
subject: event.subject,
description: event.description,
start: event.start,
end: event.end,
attendees: event.attendees
}
})
}
}
}
// Find time when entire team is available
async function findTeamSlot(
orgSlug: string,
duration: number,
startDate: Date,
endDate: Date
) {
// Option 1: Get raw busy times for manual processing
const busyTimes = await recal.calendar.getOrgWideBusy(
orgSlug,
startDate,
endDate,
true // Only check primary calendars
)
// Process busyTimes array to find gaps for your needs
// Option 2: Use the scheduling service (recommended)
const availability = await recal.scheduling.getOrgWideAvailability(
orgSlug,
startDate,
endDate,
{
slotDuration: duration,
padding: 0,
earliestTimeEachDay: '09:00',
latestTimeEachDay: '17:00'
}
)
// Returns ready-to-use available time slots
return availability.availableSlots
}
# Clone the repository
git clone https://github.com/recal-dev/recal-sdk-js.git
cd recal-sdk-js
# Install dependencies
bun install
# Run tests
bun test
# Build the SDK
bun run build
src/
├── index.ts # Main client and exports
├── services/ # Service implementations
│ ├── calendar.service.ts
│ ├── scheduling.service.ts
│ ├── users.service.ts
│ ├── organizations.service.ts
│ └── oauth.service.ts
├── entities/ # Domain models
│ ├── user.ts
│ └── organization.ts
├── types/ # TypeScript type definitions
│ ├── calendar.types.ts
│ ├── scheduling.types.ts
│ ├── internal.types.ts
│ └── oauth.types.ts
├── typebox/ # Runtime validation schemas (auto-generated)
│ ├── calendar.tb.ts
│ ├── scheduling.tb.ts
│ ├── oauth.tb.ts
│ ├── organization.tb.ts
│ ├── user.tb.ts
│ ├── timeString.tb.ts
│ ├── organization.stripped.tb.ts
│ └── user.stripped.tb.ts
├── utils/ # Helper utilities
│ ├── fetch.helper.ts
│ ├── fetchErrorHandler.ts
│ ├── includes.helper.ts
│ ├── functionize.ts
│ └── omit.ts
└── errors.ts # Custom error classes
This project uses Biome for formatting and linting:
# Format code
bun run format:fix
# Lint code
bun run lint:fix
# Run all checks
bun run check:fix
# Run all tests
bun test
# Run specific test file
bun test tests/integrations/users.test.ts
# Run with coverage
bun test --coverage
We welcome contributions! Please see our Contributing Guide for details.
git checkout -b feature/amazing-feature)bun test && bun run check:fix)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Found a bug or have a feature request? Please open an issue with:
This SDK is licensed under the MIT License. See the LICENSE file for details.
See CHANGELOG.md for a list of changes in each version.
Built with ❤️ by the Recal team
FAQs
Recal SDK
The npm package recal-sdk receives a total of 169 weekly downloads. As such, recal-sdk popularity was classified as not popular.
We found that recal-sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Following multiple malicious extension incidents, Open VSX outlines new safeguards designed to catch risky uploads earlier.

Research
/Security News
Threat actors compromised four oorzc Open VSX extensions with more than 22,000 downloads, pushing malicious versions that install a staged loader, evade Russian-locale systems, pull C2 from Solana memos, and steal macOS credentials and wallets.

Security News
Lodash 4.17.23 marks a security reset, with maintainers rebuilding governance and infrastructure to support long-term, sustainable maintenance.