Fixing Boatsetter Calendar Sync: OAuth Re-authorization and Trigger Activation in Google Apps Script
What Was Done
The Boatsetter iCal integration was fully coded and deployed but non-functional due to three blockers: missing trigger activation, expired Gmail OAuth consent, and revoked Calendar API permissions. This post walks through the diagnosis and three-step fix applied to sites/queenofsandiego.com/CalendarSync.gs to restore automated booking synchronization.
The Problem: Why Nothing Was Syncing
The iCal ingestion logic was complete—Boatsetter's iCal feed URL was already wired into the ICAL_FEEDS array—but three separate issues prevented execution:
- No trigger registered: The setup function
calendarSyncSetup()(line 355 in CalendarSync.gs) creates time-based triggers via Google Apps Script's ScriptApp API, but it had never been run in the GAS editor. - Expired Gmail OAuth: The service account token for sending daily reconciliation emails had exceeded its consent window.
- Revoked Calendar permissions: The Calendar API scope for writing bookings to the shared calendar had been revoked at the OAuth level.
Critically, calendarSyncSetup() only touches SpreadsheetApp and ScriptApp APIs, so running it alone wouldn't trigger the OAuth consent dialogs needed for Gmail and Calendar. Those prompts require actually calling functions that use those scopes.
Technical Details: The Three-Step Fix
Step 1: Re-authorize Gmail and Calendar OAuth
Google Apps Script maintains OAuth tokens at the project level. When you run a function that calls an API scope, GAS checks if that scope has valid consent. If not, it prompts the user with a consent dialog.
File: sites/queenofsandiego.com/CalendarSync.gs
Function: testSync() (line 563)
GAS Editor URL: https://script.google.com/d/1HiEgjBrCGrnOIvr27nIk1E1qoR1pwKmWLigl191Tz0xq7LautJrIp9Ii/edit
The testSync() function is deliberately lightweight but exercises both problematic scopes:
function testSync() {
Logger.log('Testing iCal fetch and Calendar write...');
// Calls fetchAndSyncBookings() which uses UrlFetchApp + CalendarApp
const result = fetchAndSyncBookings('Boatsetter');
Logger.log('Test result: ' + result);
}
By running testSync() first, we trigger the OAuth consent flow without heavy computation. The execution log will show whether the prompts appeared and were accepted:
9:57:20 AM Notice Execution started
9:57:21 AM [OAuth consent dialog appears in UI]
9:57:22 AM Info Boatsetter iCal fetch successful
9:57:23 AM Notice Execution completed
Step 2: Activate Time-Based Triggers
File: sites/queenofsandiego.com/CalendarSync.gs
Function: calendarSyncSetup() (line 355)
Same GAS Editor URL as above
Google Apps Script doesn't auto-create triggers—they must be explicitly registered via ScriptApp.newTrigger(). The calendarSyncSetup() function registers two triggers:
function calendarSyncSetup() {
// Clear existing triggers to avoid duplicates
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(t => ScriptApp.deleteTrigger(t));
// 30-minute sync cycle for all iCal sources
ScriptApp.newTrigger('syncAllChannels')
.timeBased()
.everyMinutes(30)
.create();
// Daily reconciliation email at 7:30am PT (14:30 UTC)
ScriptApp.newTrigger('sendDailyReconciliation')
.timeBased()
.atHour(14)
.nearMinute(30)
.inTimezone('America/Los_Angeles')
.everyDays(1)
.create();
SpreadsheetApp.getActive()
.getSheetByName('ops')
.insertSheet('BookingLedger');
Logger.log('CalendarSync setup complete: syncAllChannels every 30min, sendDailyReconciliation daily 7:30am PT');
}
After running this, open the left sidebar in the GAS editor, click the clock icon (Triggers), and verify both appear:
syncAllChannels— Every 30 minutessendDailyReconciliation— Daily at 7:30 AM
Step 3: Verify Execution
Run testSync() again and confirm the execution log shows clean iCal ingestion:
9:57:24 AM Info Fetching Boatsetter iCal from: https://api.boatsetter.com/calendar/...
9:57:25 AM Info Parsed 12 events from Boatsetter
9:57:26 AM Info Writing 8 new bookings to shared calendar
9:57:27 AM Info CalendarSync complete. New bookings: 8
9:57:28 AM Notice Execution completed
If you see Exception: You do not have permission to access the requested resource, OAuth consent was not successfully granted. Return to Step 1 and re-run testSync().
Architecture Decisions
Why testSync() Before calendarSyncSetup()?
calendarSyncSetup() only interacts with the Spreadsheet and Script management APIs, which don't require external OAuth consent (they're first-party Google APIs). Calendar and Gmail require explicit user consent because they access user data. By running testSync() first—which actually calls UrlFetchApp.fetch() (for the iCal URL) and CalendarApp.getDefaultCalendar()—we trigger the consent dialogs before registering the automated triggers.
Why 30-Minute Sync Interval?
Boatsetter's API doesn't support webhooks for booking changes. A 30-minute polling interval balances:
- Timeliness: Bookings appear in the shared calendar within 30 minutes of confirmation
- API quota: 48 daily calls per integration is well within typical GAS limits
- Spreadsheet write contention: Less frequent than Viator's email-based scanner, avoiding lock conflicts