Skip to main content
Cal.com apps can implement various service types depending on their functionality. Each service type provides specific interfaces and lifecycle hooks.

Service Types

Calendar Apps

Calendar apps sync booking data with external calendar services. Category: calendar Service File: lib/CalendarService.ts

Interface

import type { Calendar, CalendarEvent, IntegrationCalendar } from "@calcom/types/Calendar";

class MyCalendarService implements Calendar {
  constructor(credential: Credential) {
    // Initialize with stored credentials
  }

  async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
    // Create event in external calendar
    return {
      uid: "external-event-id",
      id: "external-event-id",
      type: "my_calendar",
      password: "",
      url: "https://example.com/event/123",
    };
  }

  async updateEvent(
    uid: string,
    event: CalendarEvent,
    externalCalendarId?: string
  ): Promise<NewCalendarEventType> {
    // Update existing event
  }

  async deleteEvent(
    uid: string,
    event: CalendarEvent,
    externalCalendarId?: string
  ): Promise<void> {
    // Delete event from external calendar
  }

  async getAvailability(
    dateFrom: string,
    dateTo: string,
    selectedCalendars: IntegrationCalendar[]
  ): Promise<EventBusyDate[]> {
    // Fetch busy times from external calendar
    return [
      {
        start: "2024-01-01T10:00:00Z",
        end: "2024-01-01T11:00:00Z",
      },
    ];
  }

  async listCalendars(): Promise<IntegrationCalendar[]> {
    // List available calendars for the user
    return [
      {
        externalId: "calendar-id",
        integration: "my_calendar",
        name: "My Calendar",
        primary: true,
      },
    ];
  }
}

export default MyCalendarService;

Registration

Export as default from lib/CalendarService.ts. The build process automatically registers it:
// Generated in calendar.services.generated.ts
export const CalendarServiceMap = {
  "my-calendar": import("./my-calendar/lib/CalendarService"),
};

OAuth Support

For OAuth-based calendars, set isOAuth: true in config.json:
{
  "isOAuth": true,
  "variant": "calendar"
}

CRM Apps

CRM apps sync contact and booking data with customer relationship management systems. Category: crm Service File: lib/CrmService.ts

Interface

import type { CRM, Contact, CRMEvent } from "@calcom/types/CRM";

class MyCrmService implements CRM {
  constructor(credential: Credential) {
    // Initialize with credentials
  }

  async createEvent(event: CalendarEvent, contacts: Contact[]): Promise<void> {
    // Create or update contacts and event in CRM
  }

  async updateEvent(uid: string, event: CalendarEvent): Promise<void> {
    // Update CRM records
  }

  async deleteEvent(uid: string): Promise<void> {
    // Delete from CRM
  }

  async getContacts(emails: string[]): Promise<Contact[]> {
    // Fetch existing contacts
    return [
      {
        email: "[email protected]",
        id: "crm-contact-id",
        name: "John Doe",
      },
    ];
  }
}

export default MyCrmService;

Example Apps

  • Attio - packages/app-store/attio/
  • Basecamp3 - packages/app-store/basecamp3/

Payment Apps

Payment apps process payments for bookings. Category: payment Service File: lib/PaymentService.ts

Interface

import type { Payment, PaymentData } from "@calcom/types/Payment";

export class PaymentService implements Payment {
  constructor(private credential: Credential) {}

  async create(payment: PaymentData) {
    // Create payment intent
    return {
      id: "payment-intent-id",
      success: false,
      url: "https://payment-provider.com/checkout/xyz",
    };
  }

  async update(paymentId: string, data: Partial<PaymentData>) {
    // Update payment
  }

  async refund(paymentId: string): Promise<PaymentData> {
    // Process refund
  }

  async afterPayment(
    event: CalendarEvent,
    booking: Booking,
    paymentData: PaymentData
  ): Promise<void> {
    // Post-payment processing (send receipts, etc.)
  }

  async deletePayment(paymentId: string): Promise<boolean> {
    // Cancel payment
  }
}

Payment Options

Define payment timing in zod.ts:
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";

export const PaymentOptions = [
  {
    label: "on_booking_option",
    value: "ON_BOOKING",
  },
  {
    label: "after_booking_option",
    value: "AFTER_BOOKING",
  },
];

export const appDataSchema = eventTypeAppCardZod.merge(
  z.object({
    price: z.number(),
    currency: z.string(),
    paymentOption: z.string().optional(),
    enabled: z.boolean().optional(),
    credentialId: z.number().optional(),
  })
);

Example Apps

  • Alby (Bitcoin) - packages/app-store/alby/
  • BTCPay Server - packages/app-store/btcpayserver/

Video Apps

Video apps provide video conferencing for bookings. Category: conferencing Service File: lib/VideoApiAdapter.ts

Interface

import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";

class MyVideoService implements VideoApiAdapter {
  constructor(private credential: Credential) {}

  async createMeeting(event: CalendarEvent): Promise<VideoCallData> {
    // Create video meeting room
    return {
      type: "my_video",
      id: "meeting-id",
      password: "meeting-password",
      url: "https://video-provider.com/join/xyz",
    };
  }

  async updateMeeting(
    bookingRef: PartialReference,
    event: CalendarEvent
  ): Promise<VideoCallData> {
    // Update meeting details
  }

  async deleteMeeting(uid: string): Promise<void> {
    // Delete meeting room
  }

  async getAvailability(dateFrom: string, dateTo: string): Promise<any[]> {
    // Optional: Check video service availability
    return [];
  }
}

export default MyVideoService;

Static Video Locations

For video services without dynamic room creation, use the event-type-location-video-static template:
{
  "variant": "conferencing",
  "appData": {
    "location": {
      "type": "my_video",
      "label": "My Video",
      "linkType": "static"
    }
  }
}

Analytics Apps

Analytics apps track booking metrics and user behavior. Category: analytics Service File: lib/AnalyticsService.ts

Interface

class MyAnalyticsService {
  constructor(private credential: Credential) {}

  async track(event: string, data: Record<string, any>): Promise<void> {
    // Send tracking event
  }

  async identify(userId: string, traits: Record<string, any>): Promise<void> {
    // Identify user
  }
}

export default MyAnalyticsService;

Tag-Based Analytics

For tag/script injection (Google Analytics, GTM, etc.), use the booking-pages-tag template:
components/TagManagerScript.tsx
export default function TagManagerScript() {
  return (
    <script
      dangerouslySetInnerHTML={{
        __html: `
          (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
          new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
          j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
          'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
          })(window,document,'script','dataLayer','GTM-XXXX');
        `,
      }}
    />
  );
}

Automation Apps

Automation apps trigger workflows based on booking events. Category: automation

Webhook-Based

Most automation apps use webhooks:
api/webhook.ts
import type { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { triggerEvent, payload } = req.body;

  switch (triggerEvent) {
    case "BOOKING_CREATED":
      // Handle booking creation
      break;
    case "BOOKING_RESCHEDULED":
      // Handle reschedule
      break;
    case "BOOKING_CANCELLED":
      // Handle cancellation
      break;
    case "MEETING_ENDED":
      // Handle meeting end
      break;
  }

  return res.status(200).json({ received: true });
}

App Features

Event Type Extensions

Apps can add UI to event type configuration using extendsFeature: "EventType":
components/EventTypeAppCardInterface.tsx
import { useFormContext } from "react-hook-form";

export default function EventTypeAppCard() {
  const { register } = useFormContext();

  return (
    <div>
      <label>
        <input type="checkbox" {...register("enabled")} />
        Enable for this event type
      </label>
    </div>
  );
}

App Settings

Provide a settings interface for installed apps:
components/AppSettingsInterface.tsx
import { useState } from "react";

export default function AppSettings({ credential }: { credential: Credential }) {
  const [settings, setSettings] = useState(credential.key);

  return (
    <div>
      <h3>App Settings</h3>
      {/* Settings form */}
    </div>
  );
}

Install Button

Custom installation flow:
components/InstallAppButton.tsx
export default function InstallAppButton() {
  const handleInstall = async () => {
    // Custom installation logic
  };

  return <button onClick={handleInstall}>Install</button>;
}

Configuration Options

config.json

Complete configuration reference:
{
  "name": "App Name",
  "slug": "app-slug",
  "type": "app-slug_category",
  "logo": "icon.svg",
  "url": "https://example.com",
  "variant": "category",
  "categories": ["category"],
  "publisher": "Publisher Name",
  "email": "[email protected]",
  "description": "Short description",
  "extendsFeature": "EventType",
  "isTemplate": false,
  "isOAuth": false,
  "dependencies": ["other-app-slug"],
  "appData": {
    "location": {
      "type": "app-slug",
      "label": "App Name",
      "linkType": "dynamic"
    },
    "tag": {
      "script": "https://example.com/script.js"
    }
  },
  "externalLink": {
    "url": "https://external-service.com",
    "newTab": true
  }
}
name
string
required
Display name shown in the app store
slug
string
required
URL-safe identifier (auto-generated from name)
type
string
required
Format: {slug}_{category}
variant
string
required
Must match category (or “conferencing” for video category)
extendsFeature
string
“EventType” - Adds settings to event types
isOAuth
boolean
default:false
Whether the app uses OAuth authentication
dependencies
string[]
Other apps required for this app to function

Best Practices

Security

  • Never expose credential.key in API responses
  • Use appKeysSchema for sensitive credentials
  • Validate all inputs with Zod schemas
  • Use environment-specific API endpoints

Performance

  • Implement caching for external API calls
  • Use lazy imports for UI components
  • Batch operations when possible
  • Handle rate limiting gracefully

Error Handling

import { ErrorWithCode } from "@calcom/lib/errorWithCode";

try {
  await externalService.createEvent(event);
} catch (error) {
  throw new ErrorWithCode(
    `Failed to create event in ${serviceName}: ${error.message}`,
    "SERVICE_ERROR"
  );
}

Testing

Test your service implementations:
import { describe, it, expect } from "vitest";
import MyCalendarService from "./CalendarService";

describe("MyCalendarService", () => {
  it("creates events", async () => {
    const service = new MyCalendarService(mockCredential);
    const result = await service.createEvent(mockEvent);
    expect(result.uid).toBeDefined();
  });
});

Examples by Type

Calendar

  • Apple Calendar - packages/app-store/applecalendar/
  • CalDAV - packages/app-store/caldavcalendar/

CRM

  • Attio - packages/app-store/attio/
  • Basecamp3 - packages/app-store/basecamp3/

Payment

  • Alby - packages/app-store/alby/
  • BTCPay Server - packages/app-store/btcpayserver/

Video

  • Huddle01 - packages/app-store/huddle01video/

Automation

  • Make - packages/app-store/make/

Next Steps

Build an App

Complete guide to building your first app

CLI Reference

App Store CLI command reference

Type Definitions

Explore TypeScript type definitions

Build docs developers (and LLMs) love