Skip to main content
Cal.com Enterprise billing provides flexible subscription management for organizations, including seat-based pricing, usage tracking, and payment processing through Stripe.

Overview

Enterprise billing features:
  • Seat-based Pricing: Pay per active user in your organization
  • Flexible Billing Periods: Monthly or annual subscriptions
  • Trial Periods: Optional trial before payment
  • Usage Tracking: Monitor booking usage and seat utilization
  • Self-Service: Manage seats and billing independently
  • Custom Pricing: Enterprise pricing for large deployments

License Requirements

Enterprise License Key

To enable Enterprise features, obtain a license key:
.env
# Required: Cal.com Enterprise license key
CALCOM_LICENSE_KEY=your_license_key

# Required: Signature token for license API
CAL_SIGNATURE_TOKEN=your_signature_token

# Optional: License API route (default shown)
CALCOM_PRIVATE_API_ROUTE=https://goblin.cal.com
Source: .env.example:21-26 To obtain a license:
  1. Visit cal.com/sales
  2. Complete the Enterprise inquiry form
  3. Receive your CALCOM_LICENSE_KEY and CAL_SIGNATURE_TOKEN
  4. Add to your .env file

License Setup

For new deployments without a license:
  1. Navigate to /auth/setup as admin
  2. Select your license type
  3. Enter license key and signature token
  4. Complete setup wizard
Source: .env.example:19

Billing Configuration

Environment Variables

Configure Stripe billing integration:
.env
# Stripe API Keys
STRIPE_PRIVATE_KEY=sk_test_xxx
STRIPE_CLIENT_ID=ca_xxx

# Organization Products
STRIPE_ORG_PRODUCT_ID=prod_xxx
STRIPE_ORG_MONTHLY_PRICE_ID=price_xxx_monthly
STRIPE_ORG_ANNUAL_PRICE_ID=price_xxx_annual

# Optional: Trial period in days
STRIPE_ORG_TRIAL_DAYS=14

# Optional: Minimum seats for self-serve (default: 30)
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30

# Optional: Price per seat (default: $37)
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37

# Monthly credits allocation for organizations
ORG_MONTHLY_CREDITS=1000

# Webhook secrets for payment events
STRIPE_WEBHOOK_SECRET=whsec_xxx
STRIPE_WEBHOOK_SECRET_BILLING=whsec_xxx_billing
Source: .env.example:206-221, .env.example:319-321, .env.example:418

Organization Billing Flow

1. Organization Creation

When creating an organization, billing details are collected:
const onboardingInput = {
  name: "Acme Corp",
  slug: "acme",
  orgOwnerEmail: "[email protected]",
  billingPeriod: "MONTHLY", // or "ANNUALLY"
  seats: 50,
  pricePerSeat: 37, // USD
};

const organizationPaymentService = new OrganizationPaymentService(user);
const onboarding = await organizationPaymentService
  .createOrganizationOnboarding(onboardingInput);
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:174-206

2. Stripe Customer Creation

A Stripe customer is created or retrieved:
// Check for existing Stripe customer
const existingCustomer = await prisma.user.findUnique({
  where: { email: orgOwnerEmail },
  select: { metadata: true },
});

const stripeCustomerId = existingCustomer?.metadata?.stripeCustomerId
  ? existingCustomer.metadata.stripeCustomerId
  : await billingService.createCustomer({
      email: orgOwnerEmail,
      metadata: { email: orgOwnerEmail },
    });
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:88-123

3. Subscription Checkout

A Stripe Checkout session is created:
const subscription = await billingService.createSubscriptionCheckout({
  customerId: stripeCustomerId,
  priceId: stripePriceId,
  quantity: seats,
  successUrl: `${WEBAPP_URL}/api/organizations/payment-redirect?session_id={CHECKOUT_SESSION_ID}&paymentStatus=success`,
  cancelUrl: `${WEBAPP_URL}/api/organizations/payment-redirect?session_id={CHECKOUT_SESSION_ID}&paymentStatus=failed`,
  metadata: {
    organizationOnboardingId: onboarding.id,
    seats,
    pricePerSeat,
    billingPeriod,
  },
  subscriptionData: {
    trial_period_days: ORG_TRIAL_DAYS, // Optional
    metadata: { source: "onboarding" },
  },
});

// Redirect user to: subscription.checkoutUrl
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:270-308

4. Payment Confirmation

After successful payment:
  1. Stripe webhook confirms payment
  2. Organization is activated
  3. Users are provisioned
  4. Subscription is active

Pricing Models

Standard Pricing

Monthly:
  • $37 per seat per month
  • Minimum 30 seats (configurable)
  • Billed monthly
Annual:
  • $37 per seat per month (billed annually)
  • Minimum 30 seats (configurable)
  • 12-month commitment
Source: .env.example:320-321

Custom Pricing

Admins can configure custom pricing:
const customOnboarding = {
  ...onboardingInput,
  pricePerSeat: 29, // Custom price
  seats: 100,
  billingPeriod: "ANNUALLY",
};

// Requires admin permission
if (user.role !== UserPermissionRole.ADMIN) {
  // Permission check fails
  throw new ErrorWithCode(
    ErrorCode.Unauthorized,
    "You do not have permission to modify the default payment settings"
  );
}
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:175-183

Custom Stripe Prices

For non-standard pricing, custom Stripe prices are created:
const customPrice = await billingService.createPrice({
  amount: pricePerSeat * 100 * occurrence, // cents * months
  productId: process.env.STRIPE_ORG_PRODUCT_ID,
  currency: "usd",
  interval: billingPeriod === "MONTHLY" ? "month" : "year",
  nickname: `Custom Organization Price - $${pricePerSeat} per seat`,
  metadata: {
    organizationOnboardingId: onboarding.id,
    pricePerSeat,
    billingPeriod,
    createdAt: new Date().toISOString(),
  },
});
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:244-268

Seat Management

Automatic Seat Calculation

Seats are automatically calculated based on:
  1. Existing Team Members: Unique members across migrated teams
  2. Invited Members: New users being added during onboarding
  3. Minimum Requirements: Configured minimum seats
// Get unique members from teams
const memberships = await prisma.membership.findMany({
  where: { teamId: { in: teamIds } },
  select: { user: { select: { email: true } } },
  distinct: ["userId"],
});

const emailsSet = new Set(memberships.map(m => m.user.email));

// Add invited members
invitedMembers.forEach(member => emailsSet.add(member.email));

const uniqueMembersCount = emailsSet.size;

// Ensure meets minimum
const seats = Math.max(configuredSeats, uniqueMembersCount);
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:137-172, .ts:358-365

Adding Seats

To add seats to an existing subscription:
  1. Navigate to Organization Settings → Billing
  2. Click “Manage Subscription”
  3. Update seat quantity
  4. Confirm changes
API Approach:
// Update subscription quantity
await stripe.subscriptions.update(subscriptionId, {
  items: [{
    id: subscriptionItemId,
    quantity: newSeatCount,
  }],
  proration_behavior: "always_invoice", // Immediate proration
});

Seat Utilization

Monitor seat usage:
  • Active Seats: Currently occupied by users
  • Available Seats: Purchased but unassigned
  • Utilization Rate: Active / Total seats
Recommended utilization: 80-90% for cost efficiency

Usage Tracking

Booking Usage Increment

Organization bookings are tracked for billing:
type PlatformOrganizationBillingTaskPayload = {
  bookingUid: string;
  organizationId: number;
  eventTypeId: number;
  scheduledTime: Date;
  // Additional metadata
};

// Increment usage when booking is created
await billingTasker.incrementUsage({
  bookingUid: booking.uid,
  organizationId: organization.id,
  eventTypeId: eventType.id,
  scheduledTime: booking.startTime,
});
Source: packages/features/ee/organizations/lib/billing/tasker/types.ts:9-26

Usage Cancellation

Cancel usage increment when booking is cancelled:
await billingTasker.cancelUsageIncrement({
  bookingUid: booking.uid,
});
Source: packages/features/ee/organizations/lib/billing/tasker/types.ts:17

Usage Rescheduling

Adjust usage tracking when booking is rescheduled:
await billingTasker.rescheduleUsageIncrement({
  bookingUid: booking.uid,
  rescheduledTime: newStartTime,
});
Source: packages/features/ee/organizations/lib/billing/tasker/types.ts:19-25

Billing Periods

Monthly Billing

const monthlyConfig = {
  billingPeriod: "MONTHLY" as BillingPeriod,
  seats: 50,
  pricePerSeat: 37,
};

// Total: 50 seats × $37 = $1,850/month

Annual Billing

const annualConfig = {
  billingPeriod: "ANNUALLY" as BillingPeriod,
  seats: 50,
  pricePerSeat: 37,
};

// Total: 50 seats × $37 × 12 months = $22,200/year
// (Paid upfront)
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:125-135

Trial Periods

Configure trial periods for new organizations:
.env
# Trial period in days (optional)
STRIPE_ORG_TRIAL_DAYS=14
Trial behavior:
  • Full access to all features during trial
  • No payment required to start trial
  • Automatic conversion to paid after trial
  • Cancel anytime during trial with no charge
Source: .env.example:221, OrganizationPaymentService.ts:288

Admin Billing Bypass

Admins can create organizations without payment:
if (user.role === UserPermissionRole.ADMIN) {
  // Skip Stripe checkout
  return {
    organizationOnboarding,
    subscription: null,
    checkoutUrl: null,
    sessionId: null,
  };
}
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:327-335 This allows:
  • Internal testing
  • Special partnerships
  • Custom billing arrangements

Webhook Configuration

Stripe Webhooks

Configure webhooks to receive billing events:
.env
# General webhook secret
STRIPE_WEBHOOK_SECRET=whsec_xxx

# Billing-specific webhook secret
STRIPE_WEBHOOK_SECRET_BILLING=whsec_xxx_billing

# App-specific webhook secret
STRIPE_WEBHOOK_SECRET_APPS=whsec_xxx_apps
Source: .env.example:223-224, .env.example:418

Webhook Events

Handle the following Stripe events:
EventAction
checkout.session.completedActivate organization subscription
customer.subscription.updatedUpdate seat count and billing details
customer.subscription.deletedSuspend organization access
invoice.payment_succeededConfirm payment and extend period
invoice.payment_failedNotify admin and suspend if needed

Webhook Endpoints

General: /api/integrations/stripepayment/webhook
Billing: /api/organizations/billing/webhook
Apps: /api/app-store/stripepayment/webhook

Organization Billing Repository

Access billing data programmatically:
import { OrganizationBilling } from "@calcom/features/ee/billing/organizations";

class InternalOrganizationBilling extends OrganizationBilling {
  async getStripeCustomerId(): Promise<string | null> {
    // Retrieve customer ID from database
    return this.repository.getStripeCustomerId(this.organization.id);
  }

  async getSubscriptionId(): Promise<string | null> {
    // Retrieve subscription ID
    return this.repository.getSubscriptionId(this.organization.id);
  }

  async getSubscriptionItems(): Promise<{ id: string; quantity: number }[]> {
    // Get subscription line items
    return this.repository.getSubscriptionItems(this.organization.id);
  }
}
Source: packages/features/ee/billing/organizations/organization-billing.ts:5-16

Troubleshooting

Payment Failed

Solutions:
  1. Verify Stripe API keys are correct
  2. Check webhook secrets match Stripe dashboard
  3. Ensure Stripe account is not in test mode (production)
  4. Verify customer payment method is valid
  5. Check for declined transactions in Stripe dashboard

Missing Stripe Configuration

Error: “STRIPE_ORG_PRODUCT_ID is not set” Solution: Configure all required Stripe environment variables:
STRIPE_PRIVATE_KEY=sk_xxx
STRIPE_ORG_PRODUCT_ID=prod_xxx
STRIPE_ORG_MONTHLY_PRICE_ID=price_xxx
STRIPE_ORG_ANNUAL_PRICE_ID=price_xxx
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:222-235

Unauthorized Custom Pricing

Error: “You do not have permission to modify the default payment settings” Solution: Only admins can set custom pricing. Either:
  1. Use standard pricing ($37/seat)
  2. Request admin to create organization with custom pricing
  3. Upgrade user to admin role
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:179-183

Webhook Not Receiving Events

Solutions:
  1. Verify webhook endpoint is publicly accessible
  2. Check webhook secret matches Stripe configuration
  3. Test webhook endpoint in Stripe dashboard
  4. Review webhook logs for errors
  5. Ensure HTTPS is enabled (required by Stripe)

Best Practices

  1. Monitor Seat Utilization: Keep utilization at 80-90% for cost efficiency
  2. Use Annual Billing: Save ~8% with annual vs monthly (typically)
  3. Enable Trial Periods: Let users experience full platform before payment
  4. Set Up Webhooks: Ensure reliable payment event handling
  5. Track Usage: Monitor booking usage to optimize seat allocation
  6. Regular Audits: Review active users and remove inactive seats
  7. Test in Staging: Always test billing flow in Stripe test mode first

API Reference

OrganizationPaymentService

class OrganizationPaymentService {
  // Create organization onboarding
  async createOrganizationOnboarding(
    input: CreateOnboardingInput
  ): Promise<OrganizationOnboarding>;

  // Create payment intent and Stripe checkout
  async createPaymentIntent(
    input: CreatePaymentIntentInput,
    organizationOnboarding: OrganizationOnboarding
  ): Promise<{
    organizationOnboarding: OrganizationOnboarding;
    subscription: StripeSubscription | null;
    checkoutUrl: string | null;
    sessionId: string | null;
  }>;
}
Source: packages/features/ee/organizations/lib/OrganizationPaymentService.ts:77-417

Build docs developers (and LLMs) love