<!-- https://unlockos.io/manual/google-calendar-integration -->

# Google Calendar Integration Help

## Overview

This page manages two types of Google Calendar integration.

| Feature | Direction | Purpose |
|---------|-----------|---------|
| **Write integration** | UnlockOS → Google Calendar | Automatically write UnlockOS native reservations to Google Calendar to manage availability in one place |
| **Read calendars** | Google Calendar → UnlockOS | Read calendars that Spacemarket, Instabase, Spacee, and similar platforms write to, and import their bookings into UnlockOS so keys and check-ins are issued automatically |

Both features require a **connected Google account**. Complete the OAuth connection by clicking "Connect Google Calendar" before configuring read calendars.

> **Google Workspace verification status:** Under review. Until verification completes, only Google accounts registered as test users on the Google OAuth consent screen can use this integration (limit: 100 accounts). Contact support if you would like to be added.

---

## How to access

Connection Settings → **External Integrations** → click the **Google Calendar** card

The legacy URL `/google-calendar-connection` automatically redirects to the new URL `/integrations/google-calendar`.

---

## Step 1: Connecting your Google account

### OAuth consent flow

You choose the write-target calendar after connecting, so there is nothing to look up or type before you click connect (the default is `primary`, your main calendar).

1. Click **"Connect Google Calendar"**
2. You are redirected to Google's OAuth consent screen
3. Review the requested permissions and click "Continue"
4. You are automatically returned to the UnlockOS settings page; status changes to "✓ Connected"
5. A "Write Calendar" section appears — change the selection there if you don't want to use your main calendar (see "Choosing the write calendar" below)

> **This is different from the previous flow.** The connect screen used to have a free-text field for the calendar ID. An operator could paste a Google Calendar browser URL into that field, and because nothing validated it, every write for that facility then silently 404'd (the page still showed "✓ Connected"). The target is now chosen from the account's real calendar list after connecting, so this class of misconfiguration can no longer happen.

### Choosing the write calendar (after connecting)

Once your Google account is connected, a **"Write Calendar"** section appears. Use its dropdown to choose which calendar UnlockOS writes native reservations to.

- The list only shows calendars the connected Google account can **actually write to** (owner or writer access). Calendars shared as read-only are excluded — offering them would just mean every write fails afterward.
- Your main calendar (`primary`) is always available as an option.
- Selecting a calendar from the dropdown saves it immediately and shows a "Write calendar updated" confirmation.
- If no calendar other than your main one has write access, a message says so — grant the connected Google account edit access to the calendar in Google Calendar's own sharing settings if you want to use a dedicated calendar.
- If the calendar list fails to load, only the main calendar is offered. Disconnecting and reconnecting may allow the list to be fetched again.

### Requested permissions

| Scope | What it allows |
|-------|----------------|
| `https://www.googleapis.com/auth/calendar.events` | Create, update, and delete Google Calendar events (write destination for UnlockOS reservations) |
| `https://www.googleapis.com/auth/calendar.calendarlist.readonly` | Read the list of calendars on your Google account (used to populate the calendar picker in the Read Calendars section) |

Both scopes are requested in a single OAuth flow. The `calendarlist.readonly` scope is read-only and is never used to read event content — only to list calendar names and IDs for the dropdown.

### Security design (CSRF protection)

When you click the Connect button, a one-time CSRF token is temporarily stored in the browser's `sessionStorage`. When Google redirects back, the token and facility ID are validated before the connection is finalized.

Private browsing mode disables `sessionStorage`, so the integration cannot be completed in a private window. Use a regular browser window.

---

## Step 2: READ — importing bookings from other platforms

The **Read Calendars** section appears below the connection status once your Google account is connected. It lets you bind a Google Calendar (such as the one that Spacemarket or Instabase writes to) to a specific room. UnlockOS then reads that calendar's events and imports them as reservations, issuing keys and check-ins automatically.

> **Prerequisite:** This section is only shown when a Google account is connected.

### Adding a new binding

1. **Source calendar** — Select the target calendar from the dropdown. All calendars on the connected Google account are listed.
   - If the dropdown is empty, see "The read calendar picker is empty or unavailable" in the Troubleshooting section below.
2. **Room to bind** — Select the room that reservations from this calendar should be assigned to.
3. Click **"Add" (追加)**.

The new binding appears in the list below.

### Automatic sync

Once a binding is created, UnlockOS syncs the linked calendar automatically approximately every 30 minutes. New events are imported as reservations.

### Manual sync

Click the **"Sync" (同期)** button on a binding card to read the calendar immediately and import any new events. The last-synced timestamp and event count update after sync completes.

### Removing a binding (解除)

Click the **"Remove" (解除)** button on a binding card and confirm in the dialog.

When a binding is removed:
- Automatic syncing of that calendar stops immediately.
- Reservations imported by this binding that have **not yet been checked in** are cancelled. Already checked-in reservations are unaffected.

### Reading the binding list

Each binding is displayed as a card with the following information:

| Field | Description |
|-------|-------------|
| Calendar ID | The Google Calendar ID being read |
| Room | The room that reservations are assigned to |
| Last synced | Date and time of the most recent successful sync |
| Status badge | **Synced** / **Error** / **Pending** |

---

## Step 3: WRITE — UnlockOS reservations in your Google Calendar

When your Google account is connected, UnlockOS native reservations are automatically written to your Google Calendar as blocking events titled **`[UnlockOS] Reserved`**. This prevents double-booking on other channels that reference the same calendar.

> **Note:** When a reservation is created or changed in UnlockOS, an immediate sync is attempted — events typically appear in Google Calendar within seconds. If the immediate dispatch fails, a backstop background job (pg_cron, approximately every 5 minutes) retries automatically. Both mechanisms are best-effort; real-time delivery is not guaranteed.

### Events written to Google Calendar

| Reservation change | Google Calendar action |
|-------------------|-----------------------|
| Reservation confirmed or checked in | Create event (or update if already exists) |
| Reservation modified (date, time, room) | Update existing event |
| Cancelled, completed, or pre-empted | Delete event |

Event titles are written as `[UnlockOS] Reserved`. The event description includes the reservation confirmation code and a link to the admin panel.

### Reservations not synced (write)

- **Reservations from iCal / OTA / PMS**: Syncing external reservations back to Google Calendar would create a circular write loop, so they are excluded
- **Past-dated reservations**: Reservations in the past are not synced
- **Draft or pending reservations**: Only reservations in `confirmed` or `checked_in` state are synced

---

## CRITICAL operating rule: 1 room = 1 calendar source

> **Do NOT connect the same calendar to a room via both an iCal feed AND a Google Calendar read binding.**

If you register the same room's availability calendar via both iCal Sync AND as a Google Calendar read binding, UnlockOS will import the same reservations twice — once from each channel. This creates **false double-booking conflicts** and duplicate reservations in the system.

**Rule: for each room, choose exactly one import channel:**

| Channel | Use when |
|---------|----------|
| iCal Sync (`?tab=ical`) | Your OTA provides an iCal (.ics) export URL |
| Google Calendar read binding (this page) | Your OTA writes directly to Google Calendar (e.g. Spacemarket, Instabase) |

If you are currently using both for the same room, remove one of them — either delete the iCal feed for that room, or remove the Google Calendar binding.

---

## Page states

The page shows one of three states depending on the integration status.

### 1. Not connected

Shown for facilities that have not yet connected.

- **"Connect Google Calendar"** button (you pick the write-target calendar from a list after connecting; the default is `primary`)

### 2. Connected

Shown for facilities where OAuth is complete and the integration is active. A green-bordered section displays the following information:

| Field | Description |
|-------|-------------|
| Google account | Email address of the Google account used to authorize |
| Connected at | Date and time when OAuth was completed |
| Pending / Failed | Count of `pending` (awaiting sync) and `failed` (retrying) reservations |

This section also shows the **"Disconnect" (連携を解除)** button.

### 3. Reauthorization required

Shown when access has been revoked on the Google account side. A red banner at the top of the page displays the number of failed syncs and a **"Reauthorize" (再認可する)** button.

Common causes of the reauthorization banner:
- Access to the app was manually revoked in Google Account settings
- A Google Workspace administrator blocked third-party app access
- Google Workspace verification has not completed and the test-user limit (100 accounts) was reached

---

## Disconnecting

1. Click **"Disconnect" (連携を解除)** in the Connected section
2. Click "OK" in the confirmation dialog

Upon disconnect:
- New reservation syncs stop immediately
- All pending and failed syncs are disabled

All stored refresh tokens (authorization credentials) are permanently deleted from Vault within 30 days of disconnect.

**Events already written to Google Calendar remain there.** Delete them manually in Google Calendar if needed.

---

## Troubleshooting

### The "Reauthorization required" banner appears

Access was revoked on the Google account side.

Steps to resolve:

1. Click **"Reauthorize" (再認可する)** to restart the OAuth flow
2. If the issue persists, check Google Account settings → Security → "Third-party apps with account access" to review UnlockOS access
3. If you use Google Workspace, ask your Workspace administrator to review third-party app policies

### Events do not appear in Google Calendar (write integration)

Check the following in order:

1. **Is the status "✓ Connected"?** — Complete the connection first if status shows "Reauthorization required" or "Not connected"
2. **Is it a native reservation?** — Reservations from OTA, iCal, or PMS are not synced via the write integration
3. **Is the intended calendar selected in the "Write Calendar" section?** — Check the dropdown's current selection
4. **Have you waited a few seconds?** — Events typically appear within seconds, but sync is best-effort. If the immediate dispatch failed, the backstop job retries within approximately 5 minutes
5. **Is the "Failed" count increasing?** — Check the pending/failed counter in the Connected section; contact support if failures persist

> **If this facility used to have a hand-typed calendar ID:** a stored value that was actually a pasted Google Calendar URL (from before the picker existed) has been automatically detected and corrected. If writes still fail, reselect the correct calendar from the "Write Calendar" dropdown.

### Clicking the connect button does nothing

If the browser's `sessionStorage` is unavailable (for example, in private browsing mode), an error message is shown. Try again in a regular browser window.

### I cancelled the Google consent screen

A message appears confirming the connection was cancelled. Start again from the "Connect Google Calendar" button.

### I want to reconnect with the same account

Click "Disconnect," then click "Connect Google Calendar" again. You can select the same Google account on the consent screen.

### The read calendar picker is empty or unavailable

The picker is empty when the `calendar.calendarlist.readonly` scope was not granted during the original OAuth connection.

Steps to resolve:

1. Click **"Disconnect" (連携を解除)** to remove the current connection
2. Click **"Connect Google Calendar"** again to reconnect — the new OAuth flow grants both scopes
3. After reconnecting, the calendar dropdown in the read calendars section will populate

While waiting to reconnect, you can manually enter a calendar ID in the text input to add a binding without the picker. If you paste a Google Calendar browser URL or sharing link by mistake, the system extracts the calendar ID from it automatically.

### Syncing a read calendar binding does not import events

Check in this order:

1. **Is the calendar ID correct?** — Compare the ID shown on the binding card against the space booking platform's calendar settings
2. **Are the events in the future?** — Past-dated events are not imported
3. **Is the Google account status "Connected"?** — If the reauthorization banner appears, reauthorize first
4. **Does the binding card show an error?** — Read the error message and contact support if it persists

### I need to cancel or edit an imported reservation

Reservations imported from a Google Calendar read binding have **Google Calendar as their source of truth**. To cancel or modify such a reservation, **edit or delete the event in Google Calendar** — do not attempt to cancel it directly from the UnlockOS admin panel. The next automatic read sync (approximately every 30 minutes) or a manual sync will reflect the change in UnlockOS.

### I see duplicate reservations for the same booking

This is the most common symptom of connecting the same room's calendar via both iCal Sync and a Google Calendar read binding simultaneously. See the [CRITICAL operating rule](#critical-operating-rule-1-room--1-calendar-source) section above and remove one of the two conflicting import channels.

---

## How the write integration works (technical reference)

### Sync mechanism

State changes to reservations in UnlockOS are automatically detected by a database trigger, which marks each affected reservation as `pending` (awaiting sync). A second AFTER trigger immediately fires an asynchronous request (`pg_net.http_post`) to the `gcal-event-sync` Edge Function the moment a reservation enters `pending` status — the request is sent after the transaction commits.

**Changes typically reflect in Google Calendar within seconds** of the reservation being created, modified, or cancelled. If the immediate dispatch fails (for example due to missing Vault secrets or a transient network error), the pg_cron backstop job (every 5 minutes) picks up any remaining `pending` rows and retries. Both mechanisms are best-effort; real-time delivery is not guaranteed.

### Retry and error handling

If a sync fails, the background job retries automatically up to 5 times. Reservations that fail all 5 attempts are recorded in the DLQ (dead-letter queue) view (`gcal_event_sync_dlq`) for operational review.

### Secure credential storage

Google refresh tokens (long-lived credentials that authorize ongoing access) are never sent to the browser. They are stored in Supabase Vault (encrypted internal storage), keyed per facility.

---

## FAQ

### Q: What happens to events already written to Google Calendar after I disconnect?

A: They remain in Google Calendar. UnlockOS does not delete them on disconnect. Delete them manually in Google Calendar if needed.

### Q: Can I import Google Calendar events into UnlockOS?

A: Yes, using the **Read Calendars** feature. Add a read calendar binding to associate a Google Calendar (for example one that Spacemarket or Instabase writes to) with a room, and UnlockOS will import those events as reservations. Note that the **write integration** itself is still one-directional: UnlockOS native reservations are pushed to Google Calendar, but Google Calendar is not polled for changes to those events.

### Q: Can each facility use a different calendar?

A: Yes. Each facility has its own independent integration. Facility A can write to `primary` while Facility B writes to a dedicated calendar ID.

### Q: Can I use multiple Google accounts?

A: One facility supports one Google account at a time. To switch accounts, disconnect and reconnect with the new account.

### Q: Where is the refresh token (authorization credential) stored?

A: In Supabase Vault (encrypted internal storage). It is never sent to the browser. It is automatically deleted within 30 days after you disconnect.

### Q: Who can authorize the integration?

A: Only Facility Owners and Organization Owners can perform the OAuth connection. Facility Members do not have access to this setting.

### Q: What is the "test user" limit?

A: While UnlockOS's Google Workspace verification is still in progress, Google accounts must be pre-registered as test users before they can connect (limit: 100 accounts). This restriction will be lifted once verification is complete. Contact support to be added as a test user.

### Q: Where do I find reservations that were imported from a read calendar binding?

A: They appear in the standard Booking Management Dashboard reservation list. The source is recorded as `adapter:gcal`, so you can distinguish them from reservations made through other channels.

### Q: What happens to imported reservations when I remove a read calendar binding?

A: Reservations imported by that binding that have not yet been checked in are cancelled. Reservations that are already checked in are not affected.

### Q: The read calendar picker is empty — how do I fix it?

A: The picker is empty when the calendar listing scope was not granted at connection time. Disconnect and reconnect your Google account (the new OAuth flow grants both scopes). Until then you can manually enter a calendar ID in the text field.

### Q: Can I bind the same calendar to more than one room?

A: Yes, but each event will be imported as a separate reservation into each bound room. Unless that is the intended behavior, it is recommended to use a 1-to-1 binding. A warning appears on the binding card if the same calendar is bound to multiple rooms.

### Q: Can I use both iCal Sync and a Google Calendar read binding for the same room?

A: No. Do not use both channels for the same room — they will import the same reservations twice and create false double-booking conflicts. Choose one import channel per room. See the [CRITICAL operating rule](#critical-operating-rule-1-room--1-calendar-source) section above.

---

## Keys for synced reservations

- Each synced event receives the key for the **room it is assigned to** (the room's
  own smart lock) — not a key from the check-in configuration's unit. In a
  multi-room property, every guest gets their own room's key.
- The key is **issued ahead of time** and KEYVOX distributes it to the lock from
  about **72 hours before check-in**.
- Synced reservations are **free at the kiosk**; selecting a price plan on the
  assigned check-in configuration does not charge these guests.
- If a calendar is bound to a room with **no smart lock assigned**, check-in is
  refused (no fallback key is issued) so the misconfiguration is visible.

## Related pages

- [External Integrations](external-integrations.md)
- [iCal Sync](ical-config.md)
- [Lock Connection](lock-connection.md)
- [neppan! Integration](integration.md)
