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 |
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 resultImplementation
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;
import requests
import os
def connect_calendar(user_id: str, provider: str, redirect_uri: str) -> str:
response = requests.get(
f'https://api.meetergo.com/v4/calendar-connections/auth/{provider}/url',
params={
'redirectUrl': redirect_uri,
'setAsCalendarSync': 'true'
},
headers={
'Authorization': f'Bearer {os.environ["MEETERGO_API_KEY"]}',
'x-meetergo-api-user-id': user_id
}
)
data = response.json()
return data['authUrl']
# Usage
auth_url = connect_calendar(
'user-uuid',
'google',
'https://yourapp.com/calendar/callback'
)
# Redirect user to OAuth (in your web framework)
return redirect(auth_url)
curl -X GET "https://api.meetergo.com/v4/calendar-connections/auth/google/url?redirectUrl=https://yourapp.com/callback&setAsCalendarSync=true" \
-H "Authorization: Bearer ak_live:uuid:secret" \
-H "x-meetergo-api-user-id: user-uuid"
<?php
function connectCalendar($userId, $provider, $redirectUri) {
$ch = curl_init();
$url = "https://api.meetergo.com/v4/calendar-connections/auth/{$provider}/url?" .
http_build_query([
'redirectUrl' => $redirectUri,
'setAsCalendarSync' => 'true'
]);
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('MEETERGO_API_KEY'),
'x-meetergo-api-user-id: ' . $userId
]
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response['authUrl'];
}
// Usage
$authUrl = connectCalendar('user-uuid', 'google', 'https://yourapp.com/callback');
header('Location: ' . $authUrl);
exit;
package main
import (
"encoding/json"
"net/http"
"net/url"
"os"
)
func connectCalendar(userID, provider, redirectURI string) (string, error) {
baseURL := "https://api.meetergo.com/v4/calendar-connections/auth/" + provider + "/url"
params := url.Values{}
params.Add("redirectUrl", redirectURI)
params.Add("setAsCalendarSync", "true")
req, _ := http.NewRequest("GET", baseURL+"?"+params.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("MEETERGO_API_KEY"))
req.Header.Set("x-meetergo-api-user-id", userID)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
AuthURL string `json:"authUrl"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.AuthURL, nil
}
Step 2: Handle the Callback
After the user authorizes access, they’ll be redirected to yourredirectUrl with query parameters:
Success:
https://yourapp.com/callback?success=true&provider=google&connectionId=abc-123
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
}
}
# Flask example
from flask import request, redirect
from urllib.parse import unquote
@app.route('/calendar/callback')
def calendar_callback():
success = request.args.get('success') == 'true'
error = request.args.get('error') == 'true'
provider = request.args.get('provider')
connection_id = request.args.get('connectionId')
message = unquote(request.args.get('message', ''))
if success:
flash(f'Successfully connected {provider} calendar!')
return redirect('/settings')
else:
flash(f'Failed to connect calendar: {message}', 'error')
return redirect('/settings?retry=calendar')
API Reference
Get OAuth URL
GET /v4/calendar-connections/auth/{provider}/url
| Parameter | Type | Required | Description |
|---|---|---|---|
provider | path | Yes | google or outlook |
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) |
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <api-key> |
x-meetergo-api-user-id | Yes | User UUID to connect calendar for |
{
"authUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
"provider": "google"
}
List Calendar Connections
GET /v4/calendar-connections/connections
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();
}
curl -X GET "https://api.meetergo.com/v4/calendar-connections/connections" \
-H "Authorization: Bearer ak_live:uuid:secret" \
-H "x-meetergo-api-user-id: user-uuid"
{
"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
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();
}
curl -X GET "https://api.meetergo.com/v4/calendar-connections/connections/conn-123/calendars" \
-H "Authorization: Bearer ak_live:uuid:secret" \
-H "x-meetergo-api-user-id: user-uuid"
{
"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}
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']
});
curl -X PATCH "https://api.meetergo.com/v4/calendar-connections/connections/conn-123" \
-H "Authorization: Bearer ak_live:uuid:secret" \
-H "x-meetergo-api-user-id: user-uuid" \
-H "Content-Type: application/json" \
-d '{"primaryCalendar": "work@group.calendar.google.com"}'
| Field | Type | Description |
|---|---|---|
primaryCalendar | string | Calendar ID to create new events in |
selectedCalendars | string[] | Calendar IDs to check for availability conflicts |
validateAvailability | boolean | Whether to check this connection for conflicts |
cancelIfDeclinedOrDeleted | boolean | Auto-cancel booking if calendar event is declined |
disableAttendeeNotification | boolean | Suppress 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
}
}
);
}
curl -X DELETE "https://api.meetergo.com/v4/calendar-connections/connections/conn-123" \
-H "Authorization: Bearer ak_live:uuid:secret" \
-H "x-meetergo-api-user-id: user-uuid"
Set Primary Calendar Connection
POST /v4/calendar-connections/connections/{connectionId}/set-primary
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;
CSS Styles
CSS Styles
.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;
}
Popup Window Flow
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);
});
}
<!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
| Error | Cause | Solution |
|---|---|---|
Connection already exists | User already connected this account | Show message that account is already connected |
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
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