Skip to main content

Cal.com vs Calendly vs Nylas: Scheduling and Calendar APIs (2026)

·PkgPulse Team

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

FeatureCal.comCalendlyNylas
Open-source
Self-hosted
Booking pages✅ (Scheduler)
Embeddable widget✅ (React)✅ (React)✅ (React)
Calendar syncGoogle, Microsoft, AppleGoogle, Microsoft, iCloudGoogle, 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 tierFree (open-source)Free (1 event type)Free (5 grants)
PricingFree / from $12/moFrom $10/seat/moFrom $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

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.