Skip to main content
Use webhooks to automatically sync booking data with your CRM or internal systems.

Overview

When a booking event occurs, meetergo sends a webhook to your endpoint. You process this webhook to create or update records in your CRM.

Setup Webhook

First, register a webhook for booking events:
curl -X POST "https://api.meetergo.com/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-meetergo-api-user-id: {userId}" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://your-server.com/webhooks/meetergo",
    "description": "CRM sync webhook",
    "eventTypes": ["booking_created", "booking_cancelled", "booking_rescheduled"]
  }'

Webhook Handler

Process incoming webhooks and sync to your CRM:
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhooks/meetergo', async (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  const { event, data } = req.body;

  try {
    switch (event) {
      case 'booking_created':
        await handleBookingCreated(data);
        break;
      case 'booking_cancelled':
        await handleBookingCancelled(data);
        break;
      case 'booking_rescheduled':
        await handleBookingRescheduled(data);
        break;
    }
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Add to retry queue if needed
  }
});

async function handleBookingCreated(data) {
  const attendee = data.attendees[0];
  const host = data.hosts[0];

  // 1. Create or update contact
  const contact = await upsertContact({
    email: attendee.email,
    firstName: attendee.firstname,
    lastName: attendee.lastname,
    phone: attendee.phone
  });

  // 2. Log the meeting activity
  await createActivity({
    contactId: contact.id,
    type: 'meeting_scheduled',
    subject: `${data.meetingType?.meetingInfo?.name || 'Meeting'} scheduled`,
    scheduledAt: data.start,
    duration: calculateDuration(data.start, data.end),
    assignedTo: host.email,
    notes: `Booking ID: ${data.id}`
  });

  // 3. Update contact properties
  await updateContact(contact.id, {
    lastMeetingScheduled: data.start,
    nextMeetingType: data.meetingType?.meetingInfo?.name
  });
}

async function handleBookingCancelled(data) {
  const attendee = data.attendees[0];

  const contact = await findContactByEmail(attendee.email);
  if (!contact) return;

  // Log cancellation
  await createActivity({
    contactId: contact.id,
    type: 'meeting_cancelled',
    subject: 'Meeting cancelled',
    notes: data.cancel?.reason || 'No reason provided',
    cancelledAt: data.cancel?.cancelledAt
  });

  // Update contact
  await updateContact(contact.id, {
    lastMeetingCancelled: new Date().toISOString()
  });
}

async function handleBookingRescheduled(data) {
  const attendee = data.rescheduledAppointment.attendees[0];

  const contact = await findContactByEmail(attendee.email);
  if (!contact) return;

  // Log reschedule
  await createActivity({
    contactId: contact.id,
    type: 'meeting_rescheduled',
    subject: 'Meeting rescheduled',
    notes: `Moved from ${data.oldStartDate} to ${data.rescheduledAppointment.start}`,
    scheduledAt: data.rescheduledAppointment.start
  });
}

function calculateDuration(start, end) {
  return Math.round((new Date(end) - new Date(start)) / 60000);
}

HubSpot Integration

Example implementation for HubSpot:
const hubspot = require('@hubspot/api-client');

const hubspotClient = new hubspot.Client({
  accessToken: process.env.HUBSPOT_ACCESS_TOKEN
});

async function upsertContact({ email, firstName, lastName, phone }) {
  try {
    // Try to find existing contact
    const searchResponse = await hubspotClient.crm.contacts.searchApi.doSearch({
      filterGroups: [{
        filters: [{
          propertyName: 'email',
          operator: 'EQ',
          value: email
        }]
      }]
    });

    if (searchResponse.results.length > 0) {
      // Update existing contact
      const contactId = searchResponse.results[0].id;
      await hubspotClient.crm.contacts.basicApi.update(contactId, {
        properties: { firstname: firstName, lastname: lastName, phone }
      });
      return { id: contactId, isNew: false };
    }

    // Create new contact
    const createResponse = await hubspotClient.crm.contacts.basicApi.create({
      properties: {
        email,
        firstname: firstName,
        lastname: lastName,
        phone
      }
    });

    return { id: createResponse.id, isNew: true };
  } catch (error) {
    console.error('HubSpot upsert failed:', error);
    throw error;
  }
}

async function createActivity({ contactId, type, subject, scheduledAt, notes }) {
  // Create engagement (meeting)
  await hubspotClient.crm.objects.meetings.basicApi.create({
    properties: {
      hs_meeting_title: subject,
      hs_meeting_start_time: new Date(scheduledAt).getTime(),
      hs_meeting_outcome: type === 'meeting_scheduled' ? 'SCHEDULED' : 'CANCELED',
      hs_meeting_body: notes
    },
    associations: [{
      to: { id: contactId },
      types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 200 }]
    }]
  });
}

Salesforce Integration

Example for Salesforce:
const jsforce = require('jsforce');

const conn = new jsforce.Connection({
  loginUrl: process.env.SF_LOGIN_URL
});

// Login on startup
conn.login(process.env.SF_USERNAME, process.env.SF_PASSWORD + process.env.SF_TOKEN);

async function upsertContact({ email, firstName, lastName, phone }) {
  // Find existing contact
  const result = await conn.sobject('Contact').findOne({ Email: email });

  if (result) {
    // Update existing
    await conn.sobject('Contact').update({
      Id: result.Id,
      FirstName: firstName,
      LastName: lastName,
      Phone: phone
    });
    return { id: result.Id, isNew: false };
  }

  // Create new contact
  const createResult = await conn.sobject('Contact').create({
    Email: email,
    FirstName: firstName,
    LastName: lastName || 'Unknown',
    Phone: phone
  });

  return { id: createResult.id, isNew: true };
}

async function createActivity({ contactId, type, subject, scheduledAt, notes, assignedTo }) {
  // Find user by email
  const user = await conn.sobject('User').findOne({ Email: assignedTo });

  // Create Event
  await conn.sobject('Event').create({
    Subject: subject,
    WhoId: contactId,
    OwnerId: user?.Id,
    StartDateTime: scheduledAt,
    EndDateTime: new Date(new Date(scheduledAt).getTime() + 30 * 60000).toISOString(),
    Description: notes,
    Type: 'Meeting'
  });
}

Pipedrive Integration

Example for Pipedrive:
const Pipedrive = require('pipedrive');

const defaultClient = Pipedrive.ApiClient.instance;
defaultClient.authentications.api_key.apiKey = process.env.PIPEDRIVE_API_KEY;

const personsApi = new Pipedrive.PersonsApi();
const activitiesApi = new Pipedrive.ActivitiesApi();

async function upsertContact({ email, firstName, lastName, phone }) {
  // Search for person by email
  const searchResult = await personsApi.searchPersons(email);

  if (searchResult.data?.items?.length > 0) {
    const personId = searchResult.data.items[0].item.id;
    await personsApi.updatePerson(personId, {
      name: `${firstName} ${lastName}`,
      phone
    });
    return { id: personId, isNew: false };
  }

  // Create new person
  const createResult = await personsApi.addPerson({
    name: `${firstName} ${lastName}`,
    email: [{ value: email, primary: true }],
    phone: phone ? [{ value: phone, primary: true }] : undefined
  });

  return { id: createResult.data.id, isNew: true };
}

async function createActivity({ contactId, type, subject, scheduledAt, notes }) {
  const activityType = type === 'meeting_scheduled' ? 'meeting' : 'call';

  await activitiesApi.addActivity({
    subject,
    type: activityType,
    person_id: contactId,
    due_date: scheduledAt.split('T')[0],
    due_time: scheduledAt.split('T')[1].substring(0, 5),
    note: notes,
    done: type === 'meeting_cancelled' ? 1 : 0
  });
}

Custom Fields Mapping

Map meetergo custom fields to CRM fields:
async function handleBookingCreated(data) {
  const attendee = data.attendees[0];
  const customFields = attendee.customFields || {};

  const contact = await upsertContact({
    email: attendee.email,
    firstName: attendee.firstname,
    lastName: attendee.lastname,
    phone: attendee.phone,
    // Map custom fields
    company: customFields.company,
    industry: customFields.industry,
    source: customFields.referral_source,
    companySize: customFields.company_size
  });

  // ...
}

Error Handling and Retries

Implement robust error handling:
const Queue = require('bull');

const webhookQueue = new Queue('webhook-processing', {
  redis: process.env.REDIS_URL
});

app.post('/webhooks/meetergo', async (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Add to queue for processing
  await webhookQueue.add(req.body, {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 2000
    }
  });
});

webhookQueue.process(async (job) => {
  const { event, data } = job.data;

  switch (event) {
    case 'booking_created':
      await handleBookingCreated(data);
      break;
    // ...
  }
});

webhookQueue.on('failed', (job, err) => {
  console.error(`Webhook job ${job.id} failed:`, err);
  // Send alert to monitoring system
});

Best Practices

Process asynchronously - Use a queue for reliability
Implement idempotency - Handle duplicate webhooks gracefully
Log everything - Keep audit trail of synced data
Handle missing data - Not all fields are always present
Use upsert patterns - Create contacts if they don’t exist
Rate limits - Respect your CRM’s API rate limits