Skip to main content
Sync meetergo bookings with external calendar systems using webhooks and calendar APIs.

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:
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

  1. Enable Google Calendar API in Google Cloud Console
  2. Create OAuth 2.0 credentials
  3. Store refresh token for server-side access

Implementation

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

  1. Register app in Azure AD
  2. Request Calendar.ReadWrite permission
  3. Store refresh token for server-side access

Implementation

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

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:
// 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:
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