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
| Event | Description |
|---|---|
booking.created | New booking created |
booking.confirmed | Host confirmed booking |
booking.cancelled | Booking cancelled |
booking.rescheduled | Booking time changed |
booking.completed | Meeting marked complete |
booking.no_show | Attendee marked as no-show |
Event Type Events
| Event | Description |
|---|---|
event_type.created | New event type created |
event_type.updated | Event type settings changed |
event_type.deleted | Event type deleted |
Schedule Events
| Event | Description |
|---|---|
schedule.updated | Availability changed |
schedule.override_added | Date override added |
Reminder Events
| Event | Description |
|---|---|
reminder.sent | Reminder delivered |
reminder.failed | Reminder 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 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 endpointWebhook 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
- API Reference - Complete API documentation
- Embedding - Add booking widgets
- Reminders - Configure notifications
On This Page
- Overview
- Event Types
- Booking Events
- Event Type Events
- Schedule Events
- Reminder Events
- Creating Webhooks
- Via API
- With Custom Headers
- Webhook Payload
- Payload Structure
- Event-Specific Payloads
- Verifying Webhooks
- Signature Headers
- Verification Code
- Handling Webhooks
- Basic Handler
- With Queue Processing
- Retry Logic
- View Delivery Logs
- Manual Retry
- Managing Webhooks
- List Webhooks
- Update Webhook
- Delete Webhook
- Rotate Secret
- Testing Webhooks
- Send Test Event
- Local Development
- Webhook Debugging
- Integration Examples
- Salesforce
- Slack Notifications
- Next Steps