Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developer.meetergo.com/llms.txt

Use this file to discover all available pages before exploring further.

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