Webhooks

Receive real-time notifications for calendar events.

Overview

Webhooks notify your application when calendar events occur. Use webhooks to sync bookings with your CRM, trigger workflows, and keep systems in sync.

Event Types

Booking Events

EventDescription
booking.createdNew booking created
booking.confirmedHost confirmed booking
booking.cancelledBooking cancelled
booking.rescheduledBooking time changed
booking.completedMeeting marked complete
booking.no_showAttendee marked as no-show

Event Type Events

EventDescription
event_type.createdNew event type created
event_type.updatedEvent type settings changed
event_type.deletedEvent type deleted

Schedule Events

EventDescription
schedule.updatedAvailability changed
schedule.override_addedDate override added

Reminder Events

EventDescription
reminder.sentReminder delivered
reminder.failedReminder delivery failed

Creating Webhooks

Via API

TypeScript:

const webhook = await client.calendar.webhooks.create({
  url: 'https://yourapp.com/webhooks/calendar',
  events: [
    'booking.created',
    'booking.cancelled',
    'booking.rescheduled',
  ],
});
 
console.log('Webhook ID:', webhook.id);
console.log('Signing secret:', webhook.secret);

Python:

webhook = client.calendar.webhooks.create(
    url="https://yourapp.com/webhooks/calendar",
    events=[
        "booking.created",
        "booking.cancelled",
        "booking.rescheduled",
    ],
)
 
print(f"Webhook ID: {webhook.id}")
print(f"Signing secret: {webhook.secret}")

With Custom Headers

const webhook = await client.calendar.webhooks.create({
  url: 'https://yourapp.com/webhooks/calendar',
  events: ['booking.created'],
  headers: {
    'X-Custom-Token': 'your-verification-token',
  },
});

Webhook Payload

Payload Structure

{
  "id": "evt_xxx",
  "type": "booking.created",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "booking": {
      "uid": "booking_xxx",
      "eventTypeId": "evt_demo",
      "eventTypeSlug": "demo",
      "status": "CONFIRMED",
      "startTime": "2024-01-20T14:00:00.000Z",
      "endTime": "2024-01-20T14:30:00.000Z",
      "attendee": {
        "name": "John Doe",
        "email": "john@example.com",
        "timezone": "America/New_York"
      },
      "host": {
        "id": "user_xxx",
        "name": "Jane Smith",
        "email": "jane@yourcompany.com"
      },
      "meetingUrl": "https://meet.google.com/xxx",
      "responses": {
        "company": "Acme Corp"
      },
      "metadata": {
        "source": "website"
      },
      "createdAt": "2024-01-15T10:30:00.000Z"
    }
  }
}

Event-Specific Payloads

booking.cancelled:

{
  "type": "booking.cancelled",
  "data": {
    "booking": { ... },
    "cancellation": {
      "reason": "Schedule conflict",
      "cancelledBy": "attendee",
      "cancelledAt": "2024-01-16T09:00:00.000Z"
    }
  }
}

booking.rescheduled:

{
  "type": "booking.rescheduled",
  "data": {
    "booking": { ... },
    "previousTime": {
      "startTime": "2024-01-20T14:00:00.000Z",
      "endTime": "2024-01-20T14:30:00.000Z"
    },
    "newTime": {
      "startTime": "2024-01-22T10:00:00.000Z",
      "endTime": "2024-01-22T10:30:00.000Z"
    },
    "rescheduledBy": "attendee",
    "reason": "Need to move to later in the week"
  }
}

Verifying Webhooks

All webhooks are signed with HMAC-SHA256. Always verify signatures.

Signature Headers

X-Transactional-Signature: sha256=xxxxx
X-Transactional-Timestamp: 1705312200

Verification Code

TypeScript/Node.js:

import crypto from 'crypto';
 
function verifyWebhook(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // Reject old timestamps (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }
 
  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
 
  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}
 
// Express middleware
app.post('/webhooks/calendar', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-transactional-signature'] as string;
  const timestamp = req.headers['x-transactional-timestamp'] as string;
 
  if (!verifyWebhook(req.body.toString(), signature, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(req.body);
  handleWebhookEvent(event);
 
  res.status(200).send('OK');
});

Python:

import hmac
import hashlib
import time
 
def verify_webhook(payload: str, signature: str, timestamp: str, secret: str) -> bool:
    # Reject old timestamps
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False
 
    # Compute expected signature
    signed_payload = f"{timestamp}.{payload}"
    expected = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
 
    # Compare signatures
    return hmac.compare_digest(signature, f"sha256={expected}")

Handling Webhooks

Basic Handler

async function handleWebhookEvent(event: WebhookEvent) {
  switch (event.type) {
    case 'booking.created':
      await handleNewBooking(event.data.booking);
      break;
 
    case 'booking.cancelled':
      await handleCancellation(event.data);
      break;
 
    case 'booking.rescheduled':
      await handleReschedule(event.data);
      break;
 
    case 'booking.completed':
      await handleCompletion(event.data.booking);
      break;
  }
}
 
async function handleNewBooking(booking: Booking) {
  // Sync to CRM
  await crm.contacts.upsert({
    email: booking.attendee.email,
    name: booking.attendee.name,
    lastMeeting: booking.startTime,
  });
 
  // Send to Slack
  await slack.notify({
    channel: '#sales',
    text: `New booking: ${booking.attendee.name} - ${booking.eventTypeSlug}`,
  });
 
  // Update analytics
  analytics.track('booking_created', {
    eventType: booking.eventTypeSlug,
    source: booking.metadata?.source,
  });
}

With Queue Processing

For reliability, queue webhooks for processing:

app.post('/webhooks/calendar', async (req, res) => {
  // Verify signature
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid');
  }
 
  // Queue for processing
  await queue.add('calendar-webhook', {
    event: req.body,
    receivedAt: new Date().toISOString(),
  });
 
  // Acknowledge immediately
  res.status(200).send('OK');
});
 
// Process in background
queue.process('calendar-webhook', async (job) => {
  const { event } = job.data;
  await handleWebhookEvent(event);
});

Retry Logic

Failed webhook deliveries are retried automatically:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After 5 failed attempts, the delivery is marked as failed.

View Delivery Logs

const deliveries = await client.calendar.webhooks.listDeliveries(webhook.id, {
  status: 'FAILED',
  count: 50,
});
 
for (const delivery of deliveries) {
  console.log(`Event: ${delivery.eventType}`);
  console.log(`Status: ${delivery.status}`);
  console.log(`Attempts: ${delivery.attemptCount}`);
  console.log(`Error: ${delivery.error}`);
}

Manual Retry

await client.calendar.webhooks.retryDelivery(delivery.id);

Managing Webhooks

List Webhooks

const webhooks = await client.calendar.webhooks.list();
 
for (const webhook of webhooks) {
  console.log(`${webhook.url}: ${webhook.isActive ? 'Active' : 'Inactive'}`);
  console.log(`  Events: ${webhook.events.join(', ')}`);
}

Update Webhook

await client.calendar.webhooks.update(webhook.id, {
  events: ['booking.created', 'booking.cancelled'],
  isActive: true,
});

Delete Webhook

await client.calendar.webhooks.delete(webhook.id);

Rotate Secret

const { secret } = await client.calendar.webhooks.rotateSecret(webhook.id);
console.log('New secret:', secret);

Testing Webhooks

Send Test Event

await client.calendar.webhooks.test(webhook.id, {
  eventType: 'booking.created',
});

Local Development

Use a tunnel for local testing:

# Install ngrok
npm install -g ngrok
 
# Start tunnel
ngrok http 3000
 
# Use ngrok URL as webhook endpoint

Webhook Debugging

Enable verbose logging:

app.post('/webhooks/calendar', (req, res) => {
  console.log('Webhook received:', {
    headers: req.headers,
    body: req.body,
    timestamp: new Date().toISOString(),
  });
 
  // Process...
});

Integration Examples

Salesforce

async function syncToSalesforce(booking: Booking) {
  // Create or update contact
  const contact = await salesforce.sobjects('Contact').upsert({
    Email: booking.attendee.email,
  }, {
    FirstName: booking.attendee.name.split(' ')[0],
    LastName: booking.attendee.name.split(' ').slice(1).join(' '),
    Company__c: booking.responses?.company,
  });
 
  // Create event
  await salesforce.sobjects('Event').create({
    Subject: booking.eventType.title,
    StartDateTime: booking.startTime,
    EndDateTime: booking.endTime,
    WhoId: contact.id,
    Type: 'Meeting',
  });
}

Slack Notifications

async function notifySlack(booking: Booking, eventType: string) {
  const message = {
    'booking.created': `New booking: ${booking.attendee.name}`,
    'booking.cancelled': `Booking cancelled: ${booking.attendee.name}`,
    'booking.rescheduled': `Booking rescheduled: ${booking.attendee.name}`,
  };
 
  await slack.chat.postMessage({
    channel: '#meetings',
    text: message[eventType],
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*${booking.eventType.title}*\n${booking.attendee.name} (${booking.attendee.email})\n${formatTime(booking.startTime)}`,
        },
      },
    ],
  });
}

Next Steps