Skip to main content
Webhooks allow your application to receive real-time notifications when events occur in Cal.com. Instead of polling the API, webhooks push data to your server as events happen.

Overview

Cal.com webhooks deliver HTTP POST requests to your specified URL when events occur:
  • Real-time notifications for booking events
  • Automatic retries with exponential backoff
  • Signature verification for security
  • Custom payload templates for flexibility
  • Multiple webhook versions for backward compatibility

Webhook Types

Webhooks can be created at different scopes:

User Webhooks

Triggered for events related to a specific user

Event Type Webhooks

Triggered for bookings of a specific event type

Team Webhooks

Triggered for events within a team

Webhook Events

Cal.com supports webhooks for the following event triggers:

Core Booking Events

EventDescription
BOOKING_CREATEDNew booking created
BOOKING_RESCHEDULEDBooking time changed
BOOKING_CANCELLEDBooking cancelled
BOOKING_REJECTEDBooking request rejected
BOOKING_REQUESTEDNew booking requires approval
BOOKING_PAIDPayment completed for booking
BOOKING_PAYMENT_INITIATEDPayment process started
BOOKING_NO_SHOW_UPDATEDNo-show status updated

Meeting Events

EventDescription
MEETING_STARTEDVideo meeting started
MEETING_ENDEDVideo meeting ended
INSTANT_MEETINGInstant meeting created
RECORDING_READYMeeting recording available
RECORDING_TRANSCRIPTION_GENERATEDTranscription completed

Other Events

EventDescription
OOO_CREATEDOut of office entry created
FORM_SUBMITTEDRouting form submitted with booking
FORM_SUBMITTED_NO_EVENTRouting form submitted without booking
ROUTING_FORM_FALLBACK_HITNo routing rules matched
AFTER_HOSTS_CAL_VIDEO_NO_SHOWHost no-show detected
AFTER_GUESTS_CAL_VIDEO_NO_SHOWGuest no-show detected
DELEGATION_CREDENTIAL_ERRORCredential delegation error
WRONG_ASSIGNMENT_REPORTAssignment error reported

Creating Webhooks

Create a User Webhook

Endpoint: POST /v2/webhooks Request:
curl -X POST https://api.cal.com/v2/webhooks \
  -H "Authorization: Bearer cal_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberUrl": "https://your-app.com/webhooks/cal",
    "active": true,
    "triggers": [
      "BOOKING_CREATED",
      "BOOKING_RESCHEDULED",
      "BOOKING_CANCELLED"
    ],
    "secret": "your_webhook_secret"
  }'
Response:
{
  "status": "success",
  "data": {
    "id": "webhook_123",
    "subscriberUrl": "https://your-app.com/webhooks/cal",
    "active": true,
    "triggers": [
      "BOOKING_CREATED",
      "BOOKING_RESCHEDULED",
      "BOOKING_CANCELLED"
    ],
    "secret": "your_webhook_secret",
    "version": "2021-10-20",
    "createdAt": "2024-03-15T10:00:00Z"
  }
}

Create an Event Type Webhook

Endpoint: POST /v2/event-types/:eventTypeId/webhooks
curl -X POST https://api.cal.com/v2/event-types/123/webhooks \
  -H "Authorization: Bearer cal_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberUrl": "https://your-app.com/webhooks/event-type",
    "active": true,
    "triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"]
  }'

Create a Team Webhook

Endpoint: POST /v2/teams/:teamId/event-types/:eventTypeId/webhooks
curl -X POST https://api.cal.com/v2/teams/456/event-types/123/webhooks \
  -H "Authorization: Bearer cal_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberUrl": "https://your-app.com/webhooks/team",
    "active": true,
    "triggers": ["BOOKING_CREATED"]
  }'

Managing Webhooks

List Webhooks

GET /v2/webhooks
Response:
{
  "status": "success",
  "data": [
    {
      "id": "webhook_123",
      "subscriberUrl": "https://your-app.com/webhooks/cal",
      "active": true,
      "triggers": ["BOOKING_CREATED", "BOOKING_CANCELLED"],
      "version": "2021-10-20"
    }
  ]
}

Get Webhook Details

GET /v2/webhooks/:webhookId

Update Webhook

PATCH /v2/webhooks/:webhookId
Request:
curl -X PATCH https://api.cal.com/v2/webhooks/webhook_123 \
  -H "Authorization: Bearer cal_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "active": false,
    "triggers": ["BOOKING_CREATED"]
  }'

Delete Webhook

DELETE /v2/webhooks/:webhookId

Webhook Payload

Standard Payload Structure

All webhooks follow this structure:
{
  "triggerEvent": "BOOKING_CREATED",
  "createdAt": "2024-03-15T10:00:00Z",
  "payload": {
    // Event-specific data
  }
}

Booking Created Payload

{
  "triggerEvent": "BOOKING_CREATED",
  "createdAt": "2024-03-15T10:00:00Z",
  "payload": {
    "bookingId": 123,
    "uid": "booking_abc123",
    "title": "30 Minute Meeting between John Doe and Jane Smith",
    "type": "30min",
    "description": "Quick sync meeting",
    "startTime": "2024-03-20T15:00:00Z",
    "endTime": "2024-03-20T15:30:00Z",
    "status": "ACCEPTED",
    "organizer": {
      "name": "John Doe",
      "email": "[email protected]",
      "timeZone": "America/New_York",
      "utcOffset": -240
    },
    "attendees": [
      {
        "name": "Jane Smith",
        "email": "[email protected]",
        "timeZone": "America/Los_Angeles",
        "utcOffset": -420,
        "firstName": "Jane",
        "lastName": "Smith"
      }
    ],
    "location": "https://meet.google.com/abc-defg-hij",
    "responses": {
      "name": {
        "label": "your_name",
        "value": "Jane Smith"
      },
      "email": {
        "label": "email_address",
        "value": "[email protected]"
      }
    },
    "eventTitle": "30 Minute Meeting",
    "eventDescription": "A brief meeting to discuss project updates",
    "price": 0,
    "currency": "usd",
    "length": 30,
    "requiresConfirmation": false
  }
}

Booking Cancelled Payload

{
  "triggerEvent": "BOOKING_CANCELLED",
  "createdAt": "2024-03-15T11:00:00Z",
  "payload": {
    "bookingId": 123,
    "uid": "booking_abc123",
    "title": "30 Minute Meeting",
    "status": "CANCELLED",
    "cancelledBy": "[email protected]",
    "cancellationReason": "Conflict with another meeting",
    "organizer": {
      "name": "John Doe",
      "email": "[email protected]"
    },
    "attendees": [
      {
        "name": "Jane Smith",
        "email": "[email protected]"
      }
    ]
  }
}

Booking Rescheduled Payload

{
  "triggerEvent": "BOOKING_RESCHEDULED",
  "createdAt": "2024-03-15T12:00:00Z",
  "payload": {
    "bookingId": 123,
    "uid": "booking_abc123",
    "title": "30 Minute Meeting",
    "startTime": "2024-03-21T15:00:00Z",
    "endTime": "2024-03-21T15:30:00Z",
    "rescheduleId": 456,
    "rescheduleUid": "booking_def456",
    "rescheduleStartTime": "2024-03-20T15:00:00Z",
    "rescheduleEndTime": "2024-03-20T15:30:00Z",
    "rescheduledBy": "[email protected]",
    "organizer": {
      "name": "John Doe",
      "email": "[email protected]"
    },
    "attendees": [
      {
        "name": "Jane Smith",
        "email": "[email protected]"
      }
    ]
  }
}

Meeting Started Payload

{
  "triggerEvent": "MEETING_STARTED",
  "createdAt": "2024-03-20T15:00:00Z",
  "payload": {
    "booking": {
      "id": 123,
      "startTime": "2024-03-20T15:00:00Z",
      "endTime": "2024-03-20T15:30:00Z",
      "title": "30 Minute Meeting",
      "status": "ACCEPTED",
      "user": {
        "name": "John Doe",
        "email": "[email protected]",
        "timeZone": "America/New_York"
      },
      "attendees": [
        {
          "name": "Jane Smith",
          "email": "[email protected]"
        }
      ]
    }
  }
}

Webhook Signature Verification

Webhooks include a signature in the X-Cal-Signature-256 header for verification.

Verifying Webhook Signatures

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express middleware
app.post('/webhooks/cal', (req, res) => {
  const signature = req.headers['x-cal-signature-256'];
  const secret = process.env.WEBHOOK_SECRET;
  
  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook
  const { triggerEvent, payload } = req.body;
  
  console.log(`Received ${triggerEvent}:`, payload);
  
  res.status(200).send('OK');
});

Custom Payload Templates

You can customize webhook payloads using templates:
curl -X POST https://api.cal.com/v2/webhooks \
  -H "Authorization: Bearer cal_live_xxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberUrl": "https://your-app.com/webhooks",
    "active": true,
    "triggers": ["BOOKING_CREATED"],
    "payloadTemplate": "{
      \"event\": \"{{type}}\",
      \"booking\": {
        \"id\": {{bookingId}},
        \"title\": \"{{title}}\",
        \"start\": \"{{startTime}}\",
        \"end\": \"{{endTime}}\"
      },
      \"organizer\": \"{{organizer.name}}\",
      \"attendee\": \"{{attendees.0.name}}\"
    }"
  }'
Available template variables:
  • {{type}} - Event type
  • {{title}} - Booking title
  • {{bookingId}} - Booking ID
  • {{startTime}} - Start time
  • {{endTime}} - End time
  • {{organizer.name}} - Organizer name
  • {{organizer.email}} - Organizer email
  • {{attendees.0.name}} - First attendee name
  • {{attendees.0.email}} - First attendee email

Webhook Delivery

Delivery Behavior

  • Timeout: 30 seconds per attempt
  • Retries: Up to 5 attempts with exponential backoff
  • Backoff: 1s, 2s, 4s, 8s, 16s
  • Success: Any 2xx status code
  • Failure: Non-2xx status code or timeout

Responding to Webhooks

Your endpoint should:
  • Respond with a 2xx status code within 30 seconds
  • Process webhooks asynchronously if needed
  • Return quickly to avoid timeouts
app.post('/webhooks/cal', async (req, res) => {
  // Respond immediately
  res.status(200).send('OK');
  
  // Process webhook asynchronously
  processWebhookAsync(req.body).catch(console.error);
});

async function processWebhookAsync(data) {
  // Long-running processing here
  await updateDatabase(data);
  await sendNotifications(data);
}

Webhook Versions

Webhooks support versioning for backward compatibility:

Version 2021-10-20 (Current)

The current webhook payload format. See payload examples above.

Specifying Webhook Version

{
  "subscriberUrl": "https://your-app.com/webhooks",
  "active": true,
  "triggers": ["BOOKING_CREATED"],
  "version": "2021-10-20"
}

Testing Webhooks

Using Webhook Testing Tools

  1. webhook.site - Get a temporary URL for testing
  2. ngrok - Expose your local server
  3. Postman - Mock webhook server

Testing with ngrok

# Start ngrok
ngrok http 3000

# Use the ngrok URL in your webhook configuration
https://abc123.ngrok.io/webhooks/cal

Test Webhook Endpoint

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/cal', (req, res) => {
  console.log('Webhook received:');
  console.log(JSON.stringify(req.body, null, 2));
  
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Error Handling

Webhook Delivery Failures

If webhook delivery fails after all retries:
  • The webhook remains active
  • Future events will continue to trigger
  • Check webhook logs in Cal.com dashboard

Common Issues

  • Ensure your endpoint responds within 30 seconds
  • Process webhooks asynchronously
  • Return 200 status immediately
  • Use valid SSL certificates
  • Don’t use self-signed certificates
  • Ensure certificate is not expired
  • Verify webhook signature correctly
  • Use the secret provided when creating webhook
  • Check signature header name: X-Cal-Signature-256
  • Implement idempotency using booking ID
  • Track processed webhook IDs
  • Handle duplicate deliveries gracefully

Best Practices

Handle duplicate webhook deliveries:
const processedWebhooks = new Set();

app.post('/webhooks/cal', async (req, res) => {
  const { payload } = req.body;
  const webhookId = `${payload.bookingId}-${req.body.createdAt}`;
  
  if (processedWebhooks.has(webhookId)) {
    return res.status(200).send('Already processed');
  }
  
  processedWebhooks.add(webhookId);
  
  // Process webhook
  await processBooking(payload);
  
  res.status(200).send('OK');
});
Maintain webhook logs for debugging:
app.post('/webhooks/cal', async (req, res) => {
  // Log webhook
  await logWebhook({
    timestamp: new Date(),
    triggerEvent: req.body.triggerEvent,
    payload: req.body.payload,
    headers: req.headers
  });
  
  // Process webhook
  await processWebhook(req.body);
  
  res.status(200).send('OK');
});
Track webhook delivery success rates:
  • Monitor response times
  • Alert on repeated failures
  • Track processing errors
  • Review webhook logs regularly
  • Always verify webhook signatures
  • Use HTTPS only
  • Validate payload structure
  • Rate limit webhook endpoints

Next Steps

Authentication

Secure your webhook endpoints

API Reference

Browse webhook endpoints

Rate Limits

Understand webhook rate limits

Examples

See webhook implementation examples

Build docs developers (and LLMs) love