Skip to main content
Enable your users to connect their calendars to meetergo for automatic availability detection and event syncing.

Why Calendar Connection?

Connected calendars enable:
  • Conflict detection - Automatically block times when users have other events
  • Event creation - Create calendar events when bookings are made
  • Two-way sync - Keep meetergo and external calendars in sync

Supported Providers

ProviderFeatures
Google CalendarFull read/write, multiple calendars
Microsoft 365Outlook calendar, Teams integration

Quick Start

Connecting a user’s calendar is a simple 3-step OAuth flow:
1

Get OAuth URL

Call the API to get the provider’s authorization URL
2

Redirect User

Send the user to Google/Microsoft to authorize access
3

Handle Callback

meetergo handles the OAuth callback and redirects to your redirectUrl with the result

Implementation

Step 1: Get OAuth Authorization URL

async function connectCalendar(userId, provider, redirectUri) {
  const response = await fetch(
    `https://api.meetergo.com/v4/calendar-connections/auth/${provider}/url?` +
    new URLSearchParams({
      redirectUrl: redirectUri,
      setAsCalendarSync: 'true'  // Optional: auto-set as primary calendar
    }),
    {
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );

  const { authUrl } = await response.json();
  return authUrl;
}

// Usage
const authUrl = await connectCalendar(
  'user-uuid',
  'google',  // or 'outlook'
  'https://yourapp.com/calendar/callback'
);

// Redirect user to OAuth
window.location.href = authUrl;

Step 2: Handle the Callback

After the user authorizes access, they’ll be redirected to your redirectUrl with query parameters: Success:
https://yourapp.com/callback?success=true&provider=google&connectionId=abc-123
Error:
https://yourapp.com/callback?error=true&provider=google&message=Connection%2520already%2520exists
// On your callback page
function handleCalendarCallback() {
  const params = new URLSearchParams(window.location.search);

  const success = params.get('success') === 'true';
  const error = params.get('error') === 'true';
  const provider = params.get('provider');
  const connectionId = params.get('connectionId');
  const message = params.get('message');

  if (success) {
    console.log(`Connected ${provider} calendar!`, connectionId);
    // Show success message, redirect to settings, etc.
  } else if (error) {
    console.error(`Failed to connect ${provider}:`, decodeURIComponent(message));
    // Show error message with retry option
  }
}

API Reference

Get OAuth URL

GET /v4/calendar-connections/auth/{provider}/url
ParameterTypeRequiredDescription
providerpathYesgoogle or outlook
redirectUrlqueryNoURL to redirect after OAuth (defaults to meetergo)
userIdqueryNoConnect for another user (admin only)
setAsCalendarSyncqueryNoAuto-set as primary calendar (true/false)
Headers:
HeaderRequiredDescription
AuthorizationYesBearer <api-key>
x-meetergo-api-user-idYesUser UUID to connect calendar for
Response:
{
  "authUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
  "provider": "google"
}

List Calendar Connections

GET /v4/calendar-connections/connections
Get all calendar connections for a user:
async function getConnections(userId) {
  const response = await fetch(
    'https://api.meetergo.com/v4/calendar-connections/connections',
    {
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );
  return response.json();
}
Response:
{
  "connections": [
    {
      "id": "conn-123",
      "provider": "google",
      "email": "user@gmail.com",
      "isPrimary": true,
      "isConnected": true,
      "primaryCalendar": "primary",
      "selectedCalendars": ["primary"],
      "validateAvailability": true,
      "cancelIfDeclinedOrDeleted": true,
      "disableAttendeeNotification": false
    }
  ],
  "total": 1
}

Get Connection Calendars

GET /v4/calendar-connections/connections/{connectionId}/calendars
Retrieve available calendars for a connection. Use this to let users choose which calendar to write events to and which to check for availability:
async function getCalendars(userId, connectionId) {
  const response = await fetch(
    `https://api.meetergo.com/v4/calendar-connections/connections/${connectionId}/calendars`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );
  return response.json();
}
Response:
{
  "calendars": [
    { "id": "primary", "name": "Calendar", "isPrimary": true, "canEdit": true, "selected": true },
    { "id": "work@group.calendar.google.com", "name": "Work", "isPrimary": false, "canEdit": true, "selected": false }
  ],
  "connectionId": "conn-123",
  "provider": "google"
}

Update Calendar Connection

PATCH /v4/calendar-connections/connections/{connectionId}
Update settings for a connection such as which calendar to create events in, which calendars to check for availability, and notification preferences:
async function updateConnection(userId, connectionId, settings) {
  const response = await fetch(
    `https://api.meetergo.com/v4/calendar-connections/connections/${connectionId}`,
    {
      method: 'PATCH',
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(settings)
    }
  );
  return response.json();
}

// Set which calendar events are created in
await updateConnection(userId, connectionId, {
  primaryCalendar: 'work@group.calendar.google.com'
});

// Choose which calendars to check for availability
await updateConnection(userId, connectionId, {
  selectedCalendars: ['primary', 'work@group.calendar.google.com']
});
Request body (all fields optional):
FieldTypeDescription
primaryCalendarstringCalendar ID to create new events in
selectedCalendarsstring[]Calendar IDs to check for availability conflicts
validateAvailabilitybooleanWhether to check this connection for conflicts
cancelIfDeclinedOrDeletedbooleanAuto-cancel booking if calendar event is declined
disableAttendeeNotificationbooleanSuppress attendee notifications on calendar events

Delete Calendar Connection

DELETE /v4/calendar-connections/connections/{connectionId}
async function disconnectCalendar(userId, connectionId) {
  await fetch(
    `https://api.meetergo.com/v4/calendar-connections/connections/${connectionId}`,
    {
      method: 'DELETE',
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );
}

Set Primary Calendar Connection

POST /v4/calendar-connections/connections/{connectionId}/set-primary
Set which calendar connection should be used for creating new events:
await fetch(
  `https://api.meetergo.com/v4/calendar-connections/connections/${connectionId}/set-primary`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
      'x-meetergo-api-user-id': userId
    }
  }
);

Building Your Own UI

Here’s a complete React example for a calendar settings page:
import { useState, useEffect } from 'react';

const PROVIDERS = [
  { id: 'google', name: 'Google Calendar', icon: '📅', color: '#4285F4' },
  { id: 'outlook', name: 'Microsoft 365', icon: '📆', color: '#0078D4' }
];

function CalendarSettings({ userId, apiKey }) {
  const [connections, setConnections] = useState([]);
  const [loading, setLoading] = useState(true);
  const [connecting, setConnecting] = useState(null);

  const headers = {
    'Authorization': `Bearer ${apiKey}`,
    'x-meetergo-api-user-id': userId
  };

  useEffect(() => {
    loadConnections();
    handleCallbackParams();
  }, []);

  async function loadConnections() {
    try {
      const response = await fetch(
        'https://api.meetergo.com/v4/calendar-connections/connections',
        { headers }
      );
      const data = await response.json();
      setConnections(data.connections);
    } finally {
      setLoading(false);
    }
  }

  function handleCallbackParams() {
    const params = new URLSearchParams(window.location.search);
    if (params.get('success') === 'true') {
      // Clear URL params and reload connections
      window.history.replaceState({}, '', window.location.pathname);
      loadConnections();
    }
  }

  async function connect(provider) {
    setConnecting(provider);
    try {
      const response = await fetch(
        `https://api.meetergo.com/v4/calendar-connections/auth/${provider}/url?` +
        new URLSearchParams({
          redirectUrl: window.location.href,
          setAsCalendarSync: 'true'
        }),
        { headers }
      );
      const { authUrl } = await response.json();
      window.location.href = authUrl;
    } catch (error) {
      alert('Failed to start connection. Please try again.');
      setConnecting(null);
    }
  }

  async function disconnect(connectionId) {
    if (!confirm('Disconnect this calendar?')) return;

    await fetch(
      `https://api.meetergo.com/v4/calendar-connections/connections/${connectionId}`,
      { method: 'DELETE', headers }
    );
    loadConnections();
  }

  if (loading) {
    return <div className="loading">Loading calendar connections...</div>;
  }

  return (
    <div className="calendar-settings">
      <h2>Calendar Connections</h2>
      <p className="description">
        Connect your calendar to automatically check for conflicts and
        create events when meetings are booked.
      </p>

      <div className="providers">
        {PROVIDERS.map(provider => {
          const connection = connections.find(c => c.provider === provider.id);
          const isConnecting = connecting === provider.id;

          return (
            <div key={provider.id} className="provider-card">
              <div className="provider-info">
                <span className="icon">{provider.icon}</span>
                <div>
                  <strong>{provider.name}</strong>
                  {connection && (
                    <span className="email">{connection.email}</span>
                  )}
                </div>
              </div>

              {connection ? (
                <div className="actions">
                  <span className="status connected">Connected</span>
                  <button
                    onClick={() => disconnect(connection.id)}
                    className="btn-secondary"
                  >
                    Disconnect
                  </button>
                </div>
              ) : (
                <button
                  onClick={() => connect(provider.id)}
                  disabled={isConnecting}
                  className="btn-primary"
                  style={{ backgroundColor: provider.color }}
                >
                  {isConnecting ? 'Connecting...' : 'Connect'}
                </button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

export default CalendarSettings;
.calendar-settings {
  max-width: 600px;
  margin: 0 auto;
  padding: 24px;
}

.description {
  color: #666;
  margin-bottom: 24px;
}

.providers {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.provider-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  background: #fff;
}

.provider-info {
  display: flex;
  align-items: center;
  gap: 12px;
}

.provider-info .icon {
  font-size: 24px;
}

.provider-info .email {
  display: block;
  font-size: 14px;
  color: #666;
}

.actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.status.connected {
  color: #22c55e;
  font-weight: 500;
}

.btn-primary {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  color: white;
  font-weight: 500;
  cursor: pointer;
}

.btn-primary:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.btn-secondary {
  padding: 8px 16px;
  border: 1px solid #e0e0e0;
  border-radius: 6px;
  background: white;
  cursor: pointer;
}

.btn-secondary:hover {
  background: #f5f5f5;
}
For a better UX, open OAuth in a popup window instead of redirecting:
function connectCalendarPopup(userId, provider, apiKey) {
  return new Promise(async (resolve, reject) => {
    // Get OAuth URL
    const response = await fetch(
      `https://api.meetergo.com/v4/calendar-connections/auth/${provider}/url?` +
      new URLSearchParams({
        redirectUrl: `${window.location.origin}/calendar-callback.html`
      }),
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'x-meetergo-api-user-id': userId
        }
      }
    );
    const { authUrl } = await response.json();

    // Open popup
    const popup = window.open(
      authUrl,
      'calendar-connect',
      'width=600,height=700,left=100,top=100'
    );

    // Listen for callback message
    const handleMessage = (event) => {
      if (event.origin !== window.location.origin) return;

      if (event.data.type === 'calendar-connected') {
        window.removeEventListener('message', handleMessage);
        popup.close();

        if (event.data.success) {
          resolve(event.data.connectionId);
        } else {
          reject(new Error(event.data.error));
        }
      }
    };

    window.addEventListener('message', handleMessage);

    // Handle popup close
    const checkClosed = setInterval(() => {
      if (popup.closed) {
        clearInterval(checkClosed);
        window.removeEventListener('message', handleMessage);
        reject(new Error('Popup closed'));
      }
    }, 1000);
  });
}
callback page (calendar-callback.html):
<!DOCTYPE html>
<html>
<head><title>Connecting...</title></head>
<body>
  <script>
    const params = new URLSearchParams(window.location.search);
    window.opener.postMessage({
      type: 'calendar-connected',
      success: params.get('success') === 'true',
      connectionId: params.get('connectionId'),
      error: params.get('message') ? decodeURIComponent(params.get('message')) : null
    }, window.location.origin);
  </script>
</body>
</html>

Error Handling

ErrorCauseSolution
Connection already existsUser already connected this accountShow message that account is already connected
access_deniedUser declined OAuth permissionShow message explaining why calendar access is needed
invalid_scopeMissing required permissionsCheck OAuth configuration
server_errorProvider API errorRetry after a few seconds
user_not_foundInvalid x-meetergo-api-user-idVerify user exists in your meetergo account

Best Practices

Explain the benefits - Tell users why connecting their calendar improves their experience
Handle errors gracefully - OAuth can fail; show helpful messages and retry options
Show connection status - Let users see which calendars are connected and manage them
Use popup flow - Better UX than full-page redirect, keeps user context
Never expose API keys in frontend code - Make API calls from your backend, not the browser
Validate redirect URLs - Only allow redirects to your own domain

Next Steps

Availability

Configure user availability schedules

Calendar Sync Recipe

Sync bookings to external calendars via webhooks

Webhooks

Get notified when bookings are created or changed

API Reference

Full API documentation