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
iCloudApple Calendar via CalDAV
CalDAVSelf-hosted calendars (Nextcloud, etc.)
ExchangeMicrosoft Exchange Server

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?provider=google&connection_id=abc-123&success=true
Error:
https://yourapp.com/callback?provider=google&error=access_denied&success=false
// On your callback page
function handleCalendarCallback() {
  const params = new URLSearchParams(window.location.search);

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

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

API Reference

Get OAuth URL

GET /v4/calendar-connections/auth/{provider}/url
ParameterTypeRequiredDescription
providerpathYesgoogle, outlook, icloud, caldav, exchange
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
Check which calendars a user has connected:
async function getConnections(userId) {
  const response = await fetch(
    'https://api.meetergo.com/v4/calendar-connections',
    {
      headers: {
        'Authorization': `Bearer ${process.env.MEETERGO_API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );
  return response.json();
}

// Response
[
  {
    "id": "conn-123",
    "provider": "google",
    "email": "[email protected]",
    "isActive": true,
    "isPrimary": true,
    "calendars": [
      { "id": "primary", "name": "Calendar", "selected": true }
    ]
  }
]

Delete Calendar Connection

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

Set Primary Calendar

POST /v4/calendar-connections/{connectionId}/primary
Set which calendar connection should be used for creating new events:
await fetch(
  `https://api.meetergo.com/v4/calendar-connections/${connectionId}/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',
        { headers }
      );
      setConnections(await response.json());
    } 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/${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('connection_id'),
      error: params.get('error')
    }, window.location.origin);
  </script>
</body>
</html>

Error Handling

ErrorCauseSolution
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