Overview
When bookings are created, rescheduled, or cancelled in meetergo, sync those changes to:- Google Calendar
- Microsoft Outlook
- Apple Calendar (via CalDAV)
- Custom calendar systems
Webhook-Based Sync
Listen for booking events and update external calendars:Copy
app.post('/webhooks/meetergo', async (req, res) => {
res.status(200).send('OK');
const { event, data } = req.body;
switch (event) {
case 'booking_created':
await createCalendarEvent(data);
break;
case 'booking_cancelled':
await deleteCalendarEvent(data);
break;
case 'booking_rescheduled':
await updateCalendarEvent(data);
break;
}
});
Google Calendar Integration
Setup
- Enable Google Calendar API in Google Cloud Console
- Create OAuth 2.0 credentials
- Store refresh token for server-side access
Implementation
Copy
const { google } = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET
);
oauth2Client.setCredentials({
refresh_token: process.env.GOOGLE_REFRESH_TOKEN
});
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
async function createCalendarEvent(bookingData) {
const attendee = bookingData.attendees[0];
const host = bookingData.hosts[0];
const event = {
summary: `${bookingData.meetingType?.meetingInfo?.name || 'Meeting'} with ${attendee.fullname}`,
description: `
Booking ID: ${bookingData.id}
Attendee: ${attendee.fullname} (${attendee.email})
${attendee.phone ? `Phone: ${attendee.phone}` : ''}
${bookingData.meetingInfo?.meetingLink ? `Meeting Link: ${bookingData.meetingInfo.meetingLink}` : ''}
`.trim(),
start: {
dateTime: bookingData.start,
timeZone: 'UTC'
},
end: {
dateTime: bookingData.end,
timeZone: 'UTC'
},
attendees: [
{ email: attendee.email, displayName: attendee.fullname },
{ email: host.email, displayName: host.fullName }
],
reminders: {
useDefault: false,
overrides: [
{ method: 'email', minutes: 60 },
{ method: 'popup', minutes: 15 }
]
},
// Store booking ID for later reference
extendedProperties: {
private: {
meetergoBookingId: bookingData.id
}
}
};
// Add video conferencing if available
if (bookingData.meetingInfo?.meetingLink) {
event.conferenceData = {
entryPoints: [{
entryPointType: 'video',
uri: bookingData.meetingInfo.meetingLink,
label: 'Join Meeting'
}]
};
}
try {
const response = await calendar.events.insert({
calendarId: 'primary',
resource: event,
sendUpdates: 'all' // Send invites to attendees
});
console.log('Calendar event created:', response.data.id);
// Store mapping for updates/deletes
await storeEventMapping(bookingData.id, response.data.id);
} catch (error) {
console.error('Failed to create calendar event:', error);
throw error;
}
}
async function deleteCalendarEvent(bookingData) {
const googleEventId = await getGoogleEventId(bookingData.id);
if (!googleEventId) {
console.log('No Google Calendar event found for booking:', bookingData.id);
return;
}
try {
await calendar.events.delete({
calendarId: 'primary',
eventId: googleEventId,
sendUpdates: 'all'
});
console.log('Calendar event deleted:', googleEventId);
await removeEventMapping(bookingData.id);
} catch (error) {
if (error.code === 404) {
// Event already deleted
await removeEventMapping(bookingData.id);
} else {
throw error;
}
}
}
async function updateCalendarEvent(data) {
const bookingData = data.rescheduledAppointment;
const googleEventId = await getGoogleEventId(bookingData.id);
if (!googleEventId) {
// Event doesn't exist, create it
await createCalendarEvent(bookingData);
return;
}
try {
await calendar.events.patch({
calendarId: 'primary',
eventId: googleEventId,
resource: {
start: {
dateTime: bookingData.start,
timeZone: 'UTC'
},
end: {
dateTime: bookingData.end,
timeZone: 'UTC'
}
},
sendUpdates: 'all'
});
console.log('Calendar event updated:', googleEventId);
} catch (error) {
console.error('Failed to update calendar event:', error);
throw error;
}
}
Microsoft Outlook Integration
Setup
- Register app in Azure AD
- Request Calendar.ReadWrite permission
- Store refresh token for server-side access
Implementation
Copy
const { Client } = require('@microsoft/microsoft-graph-client');
const { TokenCredentialAuthenticationProvider } = require('@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials');
const { ClientSecretCredential } = require('@azure/identity');
const credential = new ClientSecretCredential(
process.env.AZURE_TENANT_ID,
process.env.AZURE_CLIENT_ID,
process.env.AZURE_CLIENT_SECRET
);
const authProvider = new TokenCredentialAuthenticationProvider(credential, {
scopes: ['https://graph.microsoft.com/.default']
});
const graphClient = Client.initWithMiddleware({ authProvider });
async function createOutlookEvent(bookingData, userId) {
const attendee = bookingData.attendees[0];
const event = {
subject: `${bookingData.meetingType?.meetingInfo?.name || 'Meeting'} with ${attendee.fullname}`,
body: {
contentType: 'HTML',
content: `
<p>Booking ID: ${bookingData.id}</p>
<p>Attendee: ${attendee.fullname} (${attendee.email})</p>
${attendee.phone ? `<p>Phone: ${attendee.phone}</p>` : ''}
${bookingData.meetingInfo?.meetingLink ? `<p><a href="${bookingData.meetingInfo.meetingLink}">Join Meeting</a></p>` : ''}
`
},
start: {
dateTime: bookingData.start,
timeZone: 'UTC'
},
end: {
dateTime: bookingData.end,
timeZone: 'UTC'
},
attendees: [
{
emailAddress: {
address: attendee.email,
name: attendee.fullname
},
type: 'required'
}
],
// Store booking ID in extended properties
singleValueExtendedProperties: [{
id: 'String {66f5a359-4659-4830-9070-00047ec6ac6e} Name MeetergoBookingId',
value: bookingData.id
}]
};
// Add online meeting if Teams
if (bookingData.meetingInfo?.channel === 'teams') {
event.isOnlineMeeting = true;
event.onlineMeetingProvider = 'teamsForBusiness';
}
try {
const response = await graphClient
.api(`/users/${userId}/events`)
.post(event);
console.log('Outlook event created:', response.id);
await storeEventMapping(bookingData.id, response.id, 'outlook');
} catch (error) {
console.error('Failed to create Outlook event:', error);
throw error;
}
}
async function deleteOutlookEvent(bookingData, userId) {
const outlookEventId = await getOutlookEventId(bookingData.id);
if (!outlookEventId) return;
try {
await graphClient
.api(`/users/${userId}/events/${outlookEventId}`)
.delete();
console.log('Outlook event deleted:', outlookEventId);
await removeEventMapping(bookingData.id, 'outlook');
} catch (error) {
console.error('Failed to delete Outlook event:', error);
}
}
CalDAV Integration (Apple Calendar, etc.)
Implementation
Copy
const { DAVClient } = require('tsdav');
const ical = require('ical-generator');
const client = new DAVClient({
serverUrl: process.env.CALDAV_SERVER_URL,
credentials: {
username: process.env.CALDAV_USERNAME,
password: process.env.CALDAV_PASSWORD
},
authMethod: 'Basic',
defaultAccountType: 'caldav'
});
async function createCalDAVEvent(bookingData) {
await client.login();
const calendars = await client.fetchCalendars();
const calendar = calendars[0]; // Use primary calendar
const attendee = bookingData.attendees[0];
const host = bookingData.hosts[0];
// Generate iCal
const cal = ical({
prodId: '//Your Company//meetergo-sync//EN',
events: [{
start: new Date(bookingData.start),
end: new Date(bookingData.end),
summary: `${bookingData.meetingType?.meetingInfo?.name || 'Meeting'} with ${attendee.fullname}`,
description: `Booking ID: ${bookingData.id}`,
organizer: {
name: host.fullName,
email: host.email
},
attendees: [{
name: attendee.fullname,
email: attendee.email,
rsvp: true
}],
uid: `meetergo-${bookingData.id}@yourdomain.com`
}]
});
try {
await client.createCalendarObject({
calendar,
filename: `meetergo-${bookingData.id}.ics`,
iCalString: cal.toString()
});
console.log('CalDAV event created for booking:', bookingData.id);
} catch (error) {
console.error('Failed to create CalDAV event:', error);
throw error;
}
}
async function deleteCalDAVEvent(bookingData) {
await client.login();
const calendars = await client.fetchCalendars();
const calendar = calendars[0];
const objects = await client.fetchCalendarObjects({
calendar,
filters: [{
'comp-filter': {
_attributes: { name: 'VCALENDAR' },
'comp-filter': {
_attributes: { name: 'VEVENT' },
'prop-filter': {
_attributes: { name: 'UID' },
'text-match': {
_attributes: { collation: 'i;octet' },
_text: `meetergo-${bookingData.id}@yourdomain.com`
}
}
}
}
}]
});
for (const obj of objects) {
await client.deleteCalendarObject({
calendarObject: obj
});
}
}
Event Mapping Storage
Store mappings between meetergo bookings and external calendar events:Copy
// Using Redis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function storeEventMapping(bookingId, externalId, provider = 'google') {
await redis.hset(`event-mapping:${bookingId}`, provider, externalId);
}
async function getGoogleEventId(bookingId) {
return redis.hget(`event-mapping:${bookingId}`, 'google');
}
async function getOutlookEventId(bookingId) {
return redis.hget(`event-mapping:${bookingId}`, 'outlook');
}
async function removeEventMapping(bookingId, provider = null) {
if (provider) {
await redis.hdel(`event-mapping:${bookingId}`, provider);
} else {
await redis.del(`event-mapping:${bookingId}`);
}
}
Multi-Calendar Sync
Sync to multiple calendars based on user preferences:Copy
async function syncBookingToCalendars(bookingData) {
const host = bookingData.hosts[0];
// Get user's calendar preferences
const preferences = await getUserCalendarPreferences(host.id);
const syncPromises = [];
if (preferences.googleCalendar) {
syncPromises.push(createCalendarEvent(bookingData));
}
if (preferences.outlook) {
syncPromises.push(createOutlookEvent(bookingData, host.id));
}
if (preferences.caldav) {
syncPromises.push(createCalDAVEvent(bookingData));
}
await Promise.allSettled(syncPromises);
}
Best Practices
Store event mappings - Track external event IDs for updates/deletes
Use unique IDs - Include booking ID in calendar event for reference
Handle failures gracefully - Don’t fail the webhook if sync fails
Respect rate limits - Calendar APIs have rate limits
Send attendee updates - Notify attendees of calendar changes
Token refresh - Calendar API tokens expire; handle refresh automatically