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
| Provider | Features |
|---|
| Google Calendar | Full read/write, multiple calendars |
| Microsoft 365 | Outlook calendar, Teams integration |
| iCloud | Apple Calendar via CalDAV |
| CalDAV | Self-hosted calendars (Nextcloud, etc.) |
| Exchange | Microsoft Exchange Server |
Quick Start
Connecting a user’s calendar is a simple 3-step OAuth flow:
Get OAuth URL
Call the API to get the provider’s authorization URL
Redirect User
Send the user to Google/Microsoft to authorize access
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
| Parameter | Type | Required | Description |
|---|
provider | path | Yes | google, outlook, icloud, caldav, exchange |
redirectUrl | query | No | URL to redirect after OAuth (defaults to meetergo) |
userId | query | No | Connect for another user (admin only) |
setAsCalendarSync | query | No | Auto-set as primary calendar (true/false) |
Headers:
| Header | Required | Description |
|---|
Authorization | Yes | Bearer <api-key> |
x-meetergo-api-user-id | Yes | User 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
| Error | Cause | Solution |
|---|
access_denied | User declined OAuth permission | Show message explaining why calendar access is needed |
invalid_scope | Missing required permissions | Check OAuth configuration |
server_error | Provider API error | Retry after a few seconds |
user_not_found | Invalid x-meetergo-api-user-id | Verify 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