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