TL;DR
Cal.com is the open-source scheduling platform — embeddable booking, calendar sync, team scheduling, workflows, API-first, self-hostable, the open alternative to Calendly. Calendly is the scheduling automation leader — polished booking pages, routing forms, team scheduling, integrations, the most recognized scheduling brand. Nylas is the calendar and email API platform — unified calendar API across Google/Microsoft/iCloud, scheduling components, build custom calendar experiences. In 2026: Cal.com for open-source self-hosted scheduling, Calendly for plug-and-play booking, Nylas for building custom calendar integrations.
Key Takeaways
- Cal.com: @calcom/embed-react ~20K weekly downloads — open-source, self-hostable, API-first
- Calendly: calendly widget ~50K weekly downloads — polished, routing forms, team scheduling
- Nylas: nylas ~30K weekly downloads — unified calendar API, Google/Microsoft/iCloud sync
- Cal.com is fully open-source with a rich API
- Calendly has the most polished end-user scheduling experience
- Nylas provides low-level calendar API access across providers
Cal.com
Cal.com — open-source scheduling:
Embed booking widget
import Cal, { getCalApi } from "@calcom/embed-react"
import { useEffect } from "react"
function BookingPage() {
useEffect(() => {
(async () => {
const cal = await getCalApi()
cal("ui", {
theme: "dark",
styles: { branding: { brandColor: "#3B82F6" } },
hideEventTypeDetails: false,
})
})()
}, [])
return (
<Cal
calLink="royce/30min"
style={{ width: "100%", height: "100%", overflow: "scroll" }}
config={{ layout: "month_view" }}
/>
)
}
// Inline embed:
function InlineBooking() {
return (
<Cal
calLink="royce/consultation"
config={{
layout: "month_view",
theme: "dark",
}}
/>
)
}
// Popup embed:
function PopupBooking() {
useEffect(() => {
(async () => {
const cal = await getCalApi()
cal("ui", { theme: "dark" })
})()
}, [])
return (
<button
data-cal-link="royce/30min"
data-cal-config='{"layout":"month_view"}'
>
Schedule a Call
</button>
)
}
API integration
// Cal.com API v2:
const CAL_API = "https://api.cal.com/v2"
const headers = {
Authorization: `Bearer ${process.env.CAL_API_KEY}`,
"Content-Type": "application/json",
"cal-api-version": "2024-08-13",
}
// List event types:
const eventTypes = await fetch(`${CAL_API}/event-types`, { headers })
const { data: types } = await eventTypes.json()
types.forEach((type: any) => {
console.log(`${type.title} — ${type.length}min — ${type.slug}`)
})
// Get available slots:
const slots = await fetch(
`${CAL_API}/slots/available?startTime=2026-03-10T00:00:00Z&endTime=2026-03-17T00:00:00Z&eventTypeId=123`,
{ headers }
)
const { data: availability } = await slots.json()
Object.entries(availability.slots).forEach(([date, times]: [string, any]) => {
console.log(`${date}: ${times.length} slots available`)
})
// Create a booking:
const booking = await fetch(`${CAL_API}/bookings`, {
method: "POST",
headers,
body: JSON.stringify({
eventTypeId: 123,
start: "2026-03-12T10:00:00Z",
attendee: {
name: "Client Name",
email: "client@example.com",
timeZone: "America/New_York",
},
metadata: {
source: "pkgpulse",
plan: "pro",
},
}),
})
const { data: newBooking } = await booking.json()
console.log(`Booking confirmed: ${newBooking.uid}`)
Webhooks and workflows
// Cal.com webhook handler:
app.post("/api/webhooks/cal", async (req, res) => {
const { triggerEvent, payload } = req.body
switch (triggerEvent) {
case "BOOKING_CREATED":
console.log(`New booking: ${payload.title}`)
console.log(`Attendee: ${payload.attendees[0].email}`)
console.log(`Time: ${payload.startTime} — ${payload.endTime}`)
// Add to CRM, send Slack notification, etc.
await notifySlack(`📅 New booking: ${payload.title}`)
break
case "BOOKING_RESCHEDULED":
console.log(`Rescheduled: ${payload.uid}`)
break
case "BOOKING_CANCELLED":
console.log(`Cancelled: ${payload.uid}`)
break
case "MEETING_ENDED":
console.log(`Meeting ended: ${payload.uid}`)
// Trigger follow-up workflow
break
}
res.sendStatus(200)
})
// Manage event types via API:
await fetch(`${CAL_API}/event-types`, {
method: "POST",
headers,
body: JSON.stringify({
title: "Package Consultation",
slug: "consultation",
length: 30,
description: "Discuss package selection and architecture",
locations: [{ type: "integrations:google:meet" }],
bookingFields: [
{ name: "company", type: "text", label: "Company Name", required: true },
{ name: "projectSize", type: "select", label: "Project Size",
options: ["Small", "Medium", "Large"], required: true },
],
}),
})
Calendly
Calendly — scheduling automation:
Embed widget
// Inline embed:
import { InlineWidget } from "react-calendly"
function BookingPage() {
return (
<InlineWidget
url="https://calendly.com/royce/30min"
styles={{ height: "700px" }}
pageSettings={{
backgroundColor: "1a1a2e",
hideEventTypeDetails: false,
hideLandingPageDetails: false,
primaryColor: "3B82F6",
textColor: "ffffff",
}}
prefill={{
email: "client@example.com",
name: "Client Name",
customAnswers: {
a1: "PkgPulse", // Custom question answers
},
}}
utm={{
utmSource: "pkgpulse",
utmMedium: "website",
utmCampaign: "consultations",
}}
/>
)
}
// Popup widget:
import { PopupWidget } from "react-calendly"
function PopupBooking() {
return (
<PopupWidget
url="https://calendly.com/royce/30min"
rootElement={document.getElementById("root")!}
text="Schedule a Call"
textColor="#ffffff"
color="#3B82F6"
/>
)
}
// Popup button:
import { PopupButton } from "react-calendly"
function BookButton() {
return (
<PopupButton
url="https://calendly.com/royce/30min"
rootElement={document.getElementById("root")!}
text="Book a Meeting"
className="btn-primary"
/>
)
}
API integration
// Calendly API v2:
const CALENDLY_API = "https://api.calendly.com"
const headers = {
Authorization: `Bearer ${process.env.CALENDLY_ACCESS_TOKEN}`,
"Content-Type": "application/json",
}
// Get current user:
const me = await fetch(`${CALENDLY_API}/users/me`, { headers })
const { resource: user } = await me.json()
console.log(`Organization: ${user.current_organization}`)
// List event types:
const eventTypes = await fetch(
`${CALENDLY_API}/event_types?user=${user.uri}&active=true`,
{ headers }
)
const { collection: types } = await eventTypes.json()
types.forEach((type: any) => {
console.log(`${type.name} — ${type.duration}min — ${type.scheduling_url}`)
})
// List scheduled events:
const events = await fetch(
`${CALENDLY_API}/scheduled_events?user=${user.uri}&min_start_time=2026-03-01T00:00:00Z&max_start_time=2026-03-31T23:59:59Z&status=active`,
{ headers }
)
const { collection: scheduled } = await events.json()
scheduled.forEach((event: any) => {
console.log(`${event.name}: ${event.start_time} — ${event.end_time}`)
})
// Get event invitees:
const invitees = await fetch(
`${CALENDLY_API}/scheduled_events/${eventUuid}/invitees`,
{ headers }
)
const { collection: attendees } = await invitees.json()
attendees.forEach((invitee: any) => {
console.log(`${invitee.name} <${invitee.email}>`)
})
Webhooks
// Create webhook subscription:
await fetch(`${CALENDLY_API}/webhook_subscriptions`, {
method: "POST",
headers,
body: JSON.stringify({
url: "https://api.pkgpulse.com/webhooks/calendly",
events: [
"invitee.created", // New booking
"invitee.canceled", // Cancellation
"routing_form_submission.created", // Form submitted
],
organization: user.current_organization,
scope: "organization",
signing_key: process.env.CALENDLY_WEBHOOK_SECRET,
}),
})
// Webhook handler:
import crypto from "crypto"
app.post("/api/webhooks/calendly", (req, res) => {
// Verify signature:
const signature = req.headers["calendly-webhook-signature"] as string
const payload = JSON.stringify(req.body)
const expected = crypto
.createHmac("sha256", process.env.CALENDLY_WEBHOOK_SECRET!)
.update(payload)
.digest("hex")
if (signature !== expected) {
return res.sendStatus(403)
}
const { event, payload: data } = req.body
switch (event) {
case "invitee.created":
console.log(`New booking: ${data.name} <${data.email}>`)
console.log(`Event: ${data.scheduled_event.name}`)
console.log(`Time: ${data.scheduled_event.start_time}`)
// Add to CRM, send welcome email, etc.
break
case "invitee.canceled":
console.log(`Cancellation: ${data.name}`)
break
}
res.sendStatus(200)
})
Nylas
Nylas — calendar and email API:
Calendar access
import Nylas from "nylas"
const nylas = new Nylas({
apiKey: process.env.NYLAS_API_KEY!,
apiUri: "https://api.us.nylas.com",
})
// List calendars for a connected account:
const calendars = await nylas.calendars.list({
identifier: grantId, // Connected account grant ID
})
calendars.data.forEach((calendar) => {
console.log(`${calendar.name} (${calendar.id}) — ${calendar.readOnly ? "read-only" : "writable"}`)
})
// List events:
const events = await nylas.events.list({
identifier: grantId,
queryParams: {
calendarId: "primary",
start: Math.floor(new Date("2026-03-01").getTime() / 1000),
end: Math.floor(new Date("2026-03-31").getTime() / 1000),
},
})
events.data.forEach((event) => {
console.log(`${event.title}: ${event.when.startTime} — ${event.when.endTime}`)
event.participants?.forEach((p) => {
console.log(` ${p.name} <${p.email}> — ${p.status}`)
})
})
Create and manage events
import Nylas from "nylas"
const nylas = new Nylas({ apiKey: process.env.NYLAS_API_KEY! })
// Create event:
const event = await nylas.events.create({
identifier: grantId,
requestBody: {
calendarId: "primary",
title: "Package Architecture Review",
description: "Review package selection and architecture decisions",
when: {
startTime: Math.floor(new Date("2026-03-12T10:00:00-04:00").getTime() / 1000),
endTime: Math.floor(new Date("2026-03-12T10:30:00-04:00").getTime() / 1000),
},
location: "Google Meet",
participants: [
{ email: "client@example.com", name: "Client" },
],
conferencing: {
provider: "Google Meet",
autocreate: {}, // Auto-create Google Meet link
},
reminders: {
useDefault: false,
overrides: [
{ reminderMinutes: 10, reminderMethod: "popup" },
{ reminderMinutes: 60, reminderMethod: "email" },
],
},
},
queryParams: { calendarId: "primary" },
})
console.log(`Event created: ${event.data.id}`)
console.log(`Meet link: ${event.data.conferencing?.details?.url}`)
// Update event:
await nylas.events.update({
identifier: grantId,
eventId: event.data.id,
requestBody: {
title: "Package Architecture Review (Updated)",
description: "Updated agenda: review React 19 migration strategy",
},
queryParams: { calendarId: "primary" },
})
// Delete event:
await nylas.events.destroy({
identifier: grantId,
eventId: event.data.id,
queryParams: { calendarId: "primary" },
})
Scheduling and availability
import Nylas from "nylas"
const nylas = new Nylas({ apiKey: process.env.NYLAS_API_KEY! })
// Get free/busy availability:
const availability = await nylas.calendars.getFreeBusy({
identifier: grantId,
requestBody: {
emails: ["royce@example.com"],
startTime: Math.floor(new Date("2026-03-10T00:00:00Z").getTime() / 1000),
endTime: Math.floor(new Date("2026-03-14T23:59:59Z").getTime() / 1000),
},
})
availability.data.forEach((account) => {
account.timeSlots?.forEach((slot) => {
console.log(`${slot.status}: ${new Date(slot.startTime * 1000).toISOString()} — ${new Date(slot.endTime * 1000).toISOString()}`)
})
})
// Nylas Scheduler — create a booking page:
const config = await nylas.scheduler.configurations.create({
identifier: grantId,
requestBody: {
participants: [
{
email: "royce@example.com",
name: "Royce",
availability: {
calendarIds: ["primary"],
},
booking: {
calendarId: "primary",
},
},
],
availability: {
durationMinutes: 30,
intervalMinutes: 15,
availabilityRules: {
defaultOpenHours: [
{
days: [1, 2, 3, 4, 5], // Mon-Fri
timezone: "America/New_York",
start: "09:00",
end: "17:00",
},
],
roundRobinGroupId: undefined,
},
},
eventBooking: {
title: "Meeting with {{invitee_name}}",
description: "Scheduled via PkgPulse",
location: "Google Meet",
conferencing: {
provider: "Google Meet",
autocreate: {},
},
},
},
})
console.log(`Scheduler config: ${config.data.id}`)
React scheduling component
import { NylasScheduling, NylasSchedulerEditor } from "@nylas/react"
function BookingWidget() {
return (
<NylasScheduling
configurationId="config-123"
schedulerApiUrl="https://api.us.nylas.com"
nylasSessionsConfig={{
clientId: process.env.NEXT_PUBLIC_NYLAS_CLIENT_ID!,
redirectUri: "https://pkgpulse.com/callback",
accessType: "offline",
}}
eventOverrides={{
timeslotConfirmed: (event) => {
console.log("Booking confirmed:", event.detail)
// Redirect, show success, etc.
},
}}
/>
)
}
// Webhook for booking events:
app.post("/api/webhooks/nylas", async (req, res) => {
const { type, data } = req.body
switch (type) {
case "event.created":
console.log(`New event: ${data.object.title}`)
break
case "event.updated":
console.log(`Event updated: ${data.object.title}`)
break
case "event.deleted":
console.log(`Event deleted: ${data.object.id}`)
break
}
res.sendStatus(200)
})
Feature Comparison
| Feature | Cal.com | Calendly | Nylas |
|---|---|---|---|
| Open-source | ✅ | ❌ | ❌ |
| Self-hosted | ✅ | ❌ | ❌ |
| Booking pages | ✅ | ✅ | ✅ (Scheduler) |
| Embeddable widget | ✅ (React) | ✅ (React) | ✅ (React) |
| Calendar sync | Google, Microsoft, Apple | Google, Microsoft, iCloud | Google, Microsoft, iCloud |
| Team scheduling | ✅ (round-robin, collective) | ✅ (round-robin, collective) | ✅ (round-robin) |
| Routing forms | ✅ | ✅ | ❌ |
| Calendar API | ✅ (REST) | ✅ (REST) | ✅ (unified API) |
| Event management | ✅ | ✅ | ✅ (full CRUD) |
| Webhooks | ✅ | ✅ | ✅ |
| Custom booking fields | ✅ | ✅ | ✅ |
| Video conferencing | ✅ (Meet, Zoom, etc.) | ✅ (Meet, Zoom, etc.) | ✅ (auto-create) |
| Workflows/automations | ✅ | ✅ | ❌ |
| White-label | ✅ (self-host) | ❌ (paid) | ✅ (API-first) |
| Email integration | ❌ | ❌ | ✅ (full Email API) |
| Free tier | Free (open-source) | Free (1 event type) | Free (5 grants) |
| Pricing | Free / from $12/mo | From $10/seat/mo | From $49/mo |
When to Use Each
Use Cal.com if:
- Want an open-source scheduling platform you can self-host
- Need full customization and white-label booking pages
- Building scheduling into your product with API-first approach
- Want to avoid vendor lock-in for scheduling infrastructure
Use Calendly if:
- Want the most polished, ready-to-use scheduling experience
- Need routing forms and advanced team scheduling workflows
- Building sales/customer-facing booking with brand recognition
- Prefer plug-and-play over API customization
Use Nylas if:
- Need unified calendar API access across Google/Microsoft/iCloud
- Building custom calendar features into your application
- Need both email and calendar integration in one API
- Want low-level calendar CRUD beyond just scheduling/booking
Self-Hosting Cal.com in Production
Cal.com's self-hosted deployment is more involved than most open-source platforms in this comparison. The main application is a Next.js monorepo that requires a PostgreSQL database, Redis for session management and queue processing, and optionally Prisma Accelerate or PgBouncer for connection pooling in high-load environments. The deployment process uses Docker images or direct Vercel/Railway/Fly.io deployment, and environment variable configuration covers dozens of parameters for email, OAuth providers, payment integrations, and video conferencing connectors. One important production consideration is that calendar synchronization (the actual reading and writing to Google Calendar or Microsoft Outlook) relies on OAuth tokens stored in your database — token refresh logic runs within the application, and a Redis connection is required for background job processing of calendar sync events. For teams self-hosting as a white-labeled scheduling product, Cal.com's managed self-hosting documentation covers most production edge cases, but plan for meaningful DevOps investment during initial setup.
TypeScript Integration and Webhook Payload Types
All three platforms expose scheduling events through webhooks, and TypeScript type safety for these payloads is an important developer experience consideration. Cal.com provides TypeScript types for its webhook payloads through the @calcom/types package, though the coverage is not complete for all webhook event types as of 2026. A common pattern is to define your own discriminated union type for the triggerEvent field and use Zod to parse and validate incoming webhook payloads before processing them. Calendly's webhook payload structure is documented but not packaged as TypeScript types officially — community type definitions exist, and Zod schemas derived from the API documentation are the standard approach. Nylas provides comprehensive TypeScript types through its official SDK, and webhook payloads from Nylas are typed as part of the @nylas/nylas-js package, making Nylas the most TypeScript-complete option for custom calendar integration development.
Calendar Sync Architecture and Conflict Detection
The core technical challenge in scheduling is determining when people are actually available, which requires reading existing calendar events and detecting conflicts in real time. Cal.com's availability calculation reads the user's connected calendars, applies busy/free markers, and then overlays the configured availability windows (business hours, buffers between meetings). The calculation runs server-side on each slot request, which means it reflects real-time calendar state but requires calendar read permissions that users must grant through OAuth. Calendly uses the same approach — connected Google or Microsoft calendars are read in real time to detect conflicts, and Calendly's routing logic prevents double-booking. Nylas provides the most granular calendar access because it's fundamentally a calendar API rather than a scheduling overlay: you can read individual events, check free/busy status, and create events directly, giving you complete control over conflict detection logic in your own application code.
Privacy and Data Handling for Calendar Data
Calendar data is among the most sensitive personal data an application can access — it reveals where people are, who they meet with, and their work patterns. This makes the data handling practices of scheduling platforms a legitimate evaluation criterion. Cal.com's self-hosted deployment keeps all calendar access tokens, event metadata, and booking history within your own infrastructure, with no data sharing with third parties beyond the calendar providers themselves. The OAuth token for reading a user's Google Calendar is stored in your Cal.com database, encrypted at rest. Calendly processes all this data on Calendly's infrastructure under their privacy policy, which is acceptable for most B2B use cases but may be a concern for healthcare or legal applications where calendar metadata is particularly sensitive. Nylas's unified calendar API means your users grant Nylas access to their calendars, and Nylas stores calendar data in its infrastructure to enable sync — Nylas is HIPAA-eligible with BAA available, making it viable for healthcare scheduling applications.
Routing Forms and Lead Qualification
A scheduling tool's routing capabilities directly affect conversion rates for sales and customer success teams. Calendly's routing forms are the most mature of the three platforms — they allow multiple condition branches based on form field answers, integrating with Salesforce and HubSpot to check CRM data before routing to the appropriate team or rep. A SaaS company can build a sophisticated routing flow that asks about company size, use case, and budget, then routes large enterprise prospects to senior AEs and smaller accounts to SMB reps, with all data logged to Salesforce. Cal.com's routing forms provide similar functionality and are available in the open-source version, though CRM integration depth is more limited compared to Calendly's native Salesforce connector. Nylas does not have a routing forms concept — it provides calendar infrastructure rather than a scheduling product, so routing logic must be built in the application layer using Nylas's availability API data.
Buffer Times, Padding, and Meeting Policies
Real-world scheduling requirements go beyond simple availability — professionals need buffer time between meetings, maximum daily meeting limits, and minimum scheduling notice to avoid back-to-back appointments or last-minute bookings. Cal.com's availability configuration supports preparation and travel time buffers between events (e.g., 15 minutes after every meeting), maximum events per day limits, and minimum scheduling notice periods (e.g., no bookings within 2 hours of the desired time). These policies are configurable per event type, so a 30-minute demo can have different buffers than a 60-minute onboarding call. Calendly implements the same set of policies through its availability settings UI and API, and its enterprise tier adds additional controls for managing these policies at the team level. Nylas provides the scheduling API primitives to implement any buffer policy in application code — your application queries availability, applies buffer calculations, and presents the resulting open slots — giving maximum flexibility at the cost of requiring custom implementation. For Cal.com's self-hosted deployment, these scheduling policies are stored in the database and enforced server-side, ensuring consistent policy application regardless of which booking surface a guest uses.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on @calcom/embed-react v1.x, react-calendly v4.x, and nylas v7.x.
Compare scheduling and developer tooling on PkgPulse →
See also: AVA vs Jest and Medusa vs Saleor vs Vendure 2026, change-case vs camelcase vs slugify.