External Calendar Sync Guide
Connect Google and Outlook calendars to Resources through delegated OAuth or Company-level Microsoft tenant consent.
When to Use
- Keep provider calendars in sync so double-bookings never occur.
- Let providers own their existing Google or Outlook workflows while the API remains the system of record.
- Connect many Microsoft 365 mailboxes after one administrator grants tenant-wide consent.
- Trigger immediate syncs before quoting availability (
syncExternal=true) or before listing calendar unavailability (GET /v3/unavailability?syncExternal=true).
Concepts
- Account ownership: A Resource connects one account from either Google or Outlook, using one authorization mode. Multiple calendars may belong to that account. Changing account, provider, or authorization mode requires explicit disconnect first.
- Delegated Connection: The existing Resource workflow connects one Google or Outlook user through OAuth. A delegated account can expose multiple read calendars plus one optional write calendar.
- Hosted Calendar Selection: Set
useOnSchedCalendarSelection: truewhen connecting to use the guest picker without an OnSched login. Omitted or false preserves existing behavior: direct callback toreturnUrl, or the authenticated portal picker when no return URL is set. - Microsoft Tenant Connection: A Company-level app-only Outlook authorization granted once by a Microsoft administrator. Mailboxes are attached individually afterward without another consent screen.
- Mailbox Connection: Maps one existing Resource email to that mailbox's default Outlook calendar. Read and write start enabled and can be toggled independently.
- Pending vs Active: Health is
PENDINGwhile OAuth, first event verification, or a selected-calendar event sync is in progress. - Read vs Write: Read calendars block time; the single write calendar receives confirmed appointments.
- Busy identity: When a Resource reads more than one calendar, the same provider event ID can exist as two distinct busy rows, one per calendar. Re-ingesting one calendar does not collapse the other.
Endpoint Overview
| Endpoint | Purpose |
|---|---|
POST /v3/resource/:id/externalCalendar | Create a connection (returns authUrl). |
PUT /v3/resource/:id/externalCalendar/:externalCalendarId/regenerateAuthUrl | Reissue an OAuth URL when the previous link expired. |
GET /v3/resource/:id/externalCalendars | List calendars for the resource (pending + active). |
DELETE /v3/resource/:id/externalCalendar/:externalCalendarId | Disconnect the account and remove imported unavailability. |
GET /v3/calendar/callback | Provider OAuth callback (public). Preserves the existing redirect by default; opens guest hosted selection when opted in. |
POST /v3/calendar/selectionSession | Exchange the callback code for a Calendar Selection Token (public). |
GET /v3/resource/:id/externalCalendars/list | Fetch provider calendars once OAuth succeeds. Optional query: provider, externalCalendarId (infer provider when only one connection exists). |
POST /v3/resource/:id/externalCalendars/select | Choose read calendars (including none) and an optional single write calendar (readCalendarIds, writeCalendarId). Optional query: provider, externalCalendarId. |
POST /v3/resource/:id/externalCalendar/createUnavailabilityBulk | Manually trigger ingestion of external events into OnSched’s Unavailability table for a date range. |
POST /v3/externalCalendarTenantConnection | Create a pending Microsoft tenant connection and return its admin-consent URL. |
GET /v3/externalCalendarTenantConnection | List tenant connections for the authenticated Company. |
GET /v3/externalCalendarTenantConnection/:id | Get one Company tenant connection. |
POST /v3/externalCalendarTenantConnection/:id/mailboxConnection | Connect or reverify one Resource mailbox. |
DELETE /v3/externalCalendarTenantConnection/:id | Delete an unused tenant connection. |
Most management routes require a machine token or dashboard JWT plus company scope. During hosted selection, GET …/externalCalendars/list, POST …/externalCalendars/select, and DELETE …/externalCalendar/:id also accept a Calendar Selection Token for the matching resource connection. GET /v3/calendar/callback and POST /v3/calendar/selectionSession are public (no Bearer token). The Microsoft tenant callback is public but accepts only short-lived, signed, single-use state generated by the create route.
Typical Setup Flow
Existing delegated Google and Outlook connections keep their current OAuth flow. With useOnSchedCalendarSelection omitted or false, the callback redirects directly to the stored returnUrl; without one, it opens the authenticated portal picker. Custom pickers can continue calling list/select through their backend with company machine-token authentication.
To opt into guest hosted Calendar Selection, set the JSON boolean useOnSchedCalendarSelection: true on the connect request, or on a nested ExternalCalendars item when creating a Resource. This choice is stored on the connection and retained during reauthorization. Set it in the request body, not as an OAuth callback query parameter.
What your application implements
- From your backend, create the connection using your company machine token:
POST /v3/resource/<resourceId>/externalCalendar
Authorization: Bearer <machine-access-token>
Content-Type: application/json
{
"provider": "google",
"useOnSchedCalendarSelection": true,
"returnUrl": "https://your-app.example/calendar-connected"
}Use "provider": "outlook" for delegated Outlook OAuth. The optional returnUrl must be an absolute HTTP or HTTPS URL; use HTTPS in production.
- Open the returned
data.authUrlin the resource user's browser so they can grant provider access. - Handle the user's arrival at your
returnUrlafter they save or skip selection. OnSched adds non-secret query facts:resourceEmail,provider,externalEmail, andexternalCalendarId. These are informational; use your backend's authenticated calendar APIs to check selected calendars. Arrival alone does not mean any calendars were selected.
OnSched handles the intervening steps automatically. Its OAuth callback stores the provider authorization and opens the hosted picker. The picker exchanges its single-use code, lists calendars, and saves the user's choices without an OnSched dashboard login. Your application does not need to implement code exchange, manage a Calendar Selection Token, or build a picker to use this hosted flow. After save or skip, OnSched returns the browser to your stored returnUrl; if you omitted it, OnSched shows its hosted success page. Neither the exchange code nor the selection token is sent to your return URL.
No new customer secret is required. OnSched manages CALENDAR_SELECTION_JWT_SECRET on its API infrastructure. Customers using OnSched's hosted service do not generate or supply this secret; keep using your existing machine-token credentials for backend API calls.
Selection API details
The hosted picker calls POST /v3/calendar/selectionSession with { "code": "…" } and receives a short-lived Calendar Selection Token plus the return URL bound at connection creation. It uses that token as Authorization: Bearer … for provider calendar listing, selection, and disconnect on the matching connection. These steps are handled by OnSched for the hosted flow above.
Dashboard operators keep the existing authenticated picker when connecting from Resource settings. Integrations with their own picker may continue calling GET /v3/resource/:id/externalCalendars/list and POST /v3/resource/:id/externalCalendars/select through their backend with a company machine token. Leave useOnSchedCalendarSelection omitted or false to preserve that flow.
After calendars are selected, availability respects their external events. To force an immediate ingest, call POST /v3/resource/:id/externalCalendar/createUnavailabilityBulk?startDate=...&endDate=.... Calendar changes clear affected availability caches automatically.
Microsoft Tenant Setup
Use this flow for organizations that manage many Microsoft 365 Resources. OnSched's Microsoft application must be configured for multi-tenant access with the Calendars.ReadWrite application permission before starting.
Company owners and admins can start the same consent flow from Company Settings → Outlook in the dashboard. After the tenant connection is ACTIVE, that tab can connect one existing Resource email at a time. Owners and admins can also select the tenant from an unconnected Resource’s calendar settings. Delegated Outlook connections are skipped, not replaced. The tab is hidden for User and Resource roles.
- Create a pending connection:
POST /v3/externalCalendarTenantConnection
Authorization: Bearer <token>
x-api-key: <company-api-key>
Content-Type: application/json
{ "provider": "outlook" }- Send the returned
adminConsentUrlto a Microsoft tenant administrator. The connection becomesACTIVEonly after Microsoft redirects from the adminconsent endpoint, OnSched completes a second Microsoft authorize hop that returns an authorization code, that code'stidis stored, and a customer-tenant application token containsCalendars.ReadWrite. The unsignedtenantquery is not used to choose the tenant. App-role assignment can lag the redirect; OnSched retries that token check before failing the connection. A transient Microsoft network error during verification leaves the connectionPENDINGso the administrator can refresh the callback. A retried Microsoft redirect after that activation leaves the connectionACTIVE. - Connect each Resource using its email. Initial setup can loop over the organization's list with modest concurrency; later one-offs use the same request:
POST /v3/externalCalendarTenantConnection/<connectionId>/mailboxConnection
Authorization: Bearer <token>
x-api-key: <company-api-key>
Content-Type: application/json
{ "email": "[email protected]" }Each request normalizes the email and requires exactly one active Resource match in that Company. A successful response means OnSched found the writable default calendar and completed an initial 30-day event sync. Repeating the request is safe. Existing delegated Outlook connections return status: "skipped" with reason: "ALREADY_CONNECTED" and are not replaced.
A Company can repeat this setup for multiple Microsoft tenants. The mailbox request always names the tenant connection it should use. The same Microsoft tenant cannot be activated on a second Company (409 MICROSOFT_TENANT_IN_USE).
Tenant-wide Microsoft permission technically authorizes the application for every mailbox in the consenting tenant. OnSched accesses only mailboxes explicitly submitted through the mailbox-connection endpoint. This application allowlist is not an Exchange-side permission boundary.
Omitting an email from a later loop does not disconnect it. Disconnect the Resource calendar, or delete the Resource, before deleting its tenant connection. Both clear local bindings, imported busy blocks, credentials, and tenant refs so tenant deletion can succeed. Remote appointment events stay. Tenant deletion returns 409 TENANT_CONNECTION_IN_USE while any Resource still references that connection.
Changing or Disconnecting an Account
Personal reauthorization must use the same account and preserves calendar IDs and selections. Generating another authorization URL supersedes earlier attempts. Appointment creates and event cleanup keep using the existing token until that new authorization completes. Use the existing disconnect endpoint before choosing a different account, provider, or personal/organization mode.
POST /v3/resource/:id/externalCalendars/select accepts readCalendarIds: [] and writeCalendarId: null to stop reading and writing without disconnecting the account. Deselected calendar rows remain listed with their flags disabled; filter on read/write when displaying selected calendars.
Organization connections use only their default calendar. Resource settings display the bound mailbox and read/write toggles instead of a calendar picker or personal OAuth controls. Admin consent alone never connects Resources or replaces personal connections. Editing Resource email does not retarget a connected mailbox; disconnect and reconnect explicitly.
Disconnect and Resource DELETE both clear the account’s calendar bindings, imported busy blocks, credentials, and tenant refs, and retire local appointment event mappings. Remote appointment events stay. After Resource DELETE, tenant deletion can succeed. A replacement connection receives new bookings; existing appointments are not automatically copied into it. Old remote events may require manual cleanup after an account switch.
If you change the write calendar, appointment event cleanup (cancel, reschedule, padding, and permanent delete of a hold or reserve) still uses the calendar that originally received the event, including when that calendar is now read-only. Cleanup also runs when no calendar currently has write selected. OnSched records that origin internally; it is not a request field on appointment create, update, reschedule, cancel, or confirm. Some older Google events cannot be located after historical calendar switches; those remote events stay untouched. Concurrent padding refreshes on the same connected calendar do not leave extra remote padding events.
Fail-Closed Availability Protection
Resources support the opt-in blockAvailabilityOnExternalCalendarFailure setting. It defaults to false.
When enabled:
GET /v3/availabilitywithsyncExternal=trueexcludes the Resource while authorization or an event sync is pending, or after its latest selected read-calendar sync failed.- Hold, book, and reschedule validation reject the slot through the normal slot-unavailable response while external events are respected.
syncExternal=falseand appointment requests withignoreExternalEvents=trueintentionally bypass both external busy events and calendar-health enforcement.- A Resource with no connected account, or a connected account with no read calendars selected, remains eligible.
- Legacy connections begin as
UNKNOWN, which is non-blocking until the first successful or failed event-sync attempt.
Authenticated Resource responses include:
{
"blockAvailabilityOnExternalCalendarFailure": true,
"externalCalendarHealth": {
"status": "HEALTHY",
"lastAttemptAt": "2026-07-16T20:00:00.000Z",
"lastSuccessAt": "2026-07-16T20:00:00.000Z",
"failureCode": null
}
}Health statuses are NOT_CONNECTED, PENDING, NOT_MONITORED, UNKNOWN, HEALTHY, and FAILED. PENDING covers incomplete OAuth, first event-read verification, and a currently running selected-calendar event sync. Failed responses expose only a sanitized failureCode: AUTH, RATE_LIMITED, TIMEOUT, PROVIDER_UNAVAILABLE, or UNKNOWN.
Data You Receive
Each calendar record contains:
provider:googleoroutlook.pending: Boolean indicating OAuth completion.read/write: Flags that determine whether events block availability or receive appointment events.calendarIdandcalendarEmail: Maps back to the provider’s identifiers.authMode:delegatedfor per-user OAuth orapplicationfor a Microsoft tenant mailbox connection.ExternalCalendarTenantConnectionId: Tenant connection used by an application-mode Outlook calendar; null for delegated connections.- Credentials, including
refreshTokenandhomeAccountId, are private and are no longer returned by calendar or Resource endpoints. Applications that previously consumed those fields must stop relying on them. - Appointment GET and list responses omit internal origin metadata used to locate events after a write-calendar switch. Provider event IDs remain. Origin is not a request field.
Ingest behavior: Provider events marked free or transparent (for example Google Working Location, Outlook showAs: free) are not stored as Unavailability and do not block availability.
Calendar view: GET /v3/unavailability rows that originate from external sync include optional external_transparency, external_event_type, and external_busy for styling or debugging busy provider events. Pass syncExternal=true to ingest provider events for resolved resources before listing; when ResourceIds is omitted, ingested external rows for linked resources may appear even if the request scoped only a location or service.
Best Practices
- Limit External Syncs:
syncExternal=trueonGET /v3/availabilityorGET /v3/unavailabilityfetches external events just-in-time but adds latency. Use it only when the UI must account for updates that happened seconds ago. On unavailability lists, the flag ingests before listing and does not hide stored external blocks whenfalse(availability slot calculation uses a different include/exclude rule). Otherwise rely on the background sync cadence triggered by bookings and periodic jobs. - Plan for Provider Failures: If a provider or OAuth issue prevents one selected read calendar from syncing, OnSched leaves previously stored external busy blocks in place and records the aggregate Resource health as failed. Enable
blockAvailabilityOnExternalCalendarFailurewhen uncertain Resources must be removed fromsyncExternal=trueresults until every selected read calendar syncs successfully. - Clean Disconnects: Disconnect a calendar, or delete the Resource, to clear local bindings, imported busy blocks, credentials, and tenant refs. Remote appointment events stay. Either path unblocks tenant deletion.
- Read vs Write: Selecting more than one write calendar is not allowed. If you change the write target, the old calendar automatically has its
writeflag cleared. Appointment event cleanup still uses the calendar that originally received the event, including when no calendar currently has write selected.
Troubleshooting
-
RESOURCE_CALENDAR_CONNECTION_CONFLICTorRESOURCE_CALENDAR_ALREADY_CONNECTED: A connection or pending setup already occupies this Resource. Disconnect it before choosing another provider or authorization mode; reauthorize an existing personal account through its regenerate URL endpoint. -
CALENDAR_ACCOUNT_CHANGED_DISCONNECT_REQUIRED: The OAuth login differs from the connected account. Use the original account or disconnect before switching. -
CALENDAR_AUTHORIZATION_SUPERSEDED: Use the most recently generated authorization URL. -
No Google calendars listed: The resource must authorize first; only then can
/listsucceed. -
Tenant connection remains
PENDING: The Microsoft administrator must finish the short-livedadminConsentUrlflow and reach OnSched's callback. If the callback returns a network verification error, refresh that same URL. Delete the unused pending connection and create another if its URL expired. A denied consent or a non-transient verification failure marks the connectionFAILED. -
RESOURCE_EMAIL_NOT_FOUND: Create or update an active Resource with the submitted email, then retry. -
RESOURCE_EMAIL_AMBIGUOUS: More than one active Resource in the Company has that email. Make Resource emails unique before retrying. -
RESOURCE_EMAIL_CHANGED_RECONNECT_REQUIRED: Resource email edits never silently retarget an existing mailbox binding. Delete its External Calendar, then connect the new email explicitly. -
RESOURCE_OUTLOOK_CONNECTION_CONFLICT: The Resource is already bound through a different Microsoft tenant connection. Delete that External Calendar before choosing another tenant. -
MICROSOFT_TENANT_IN_USE: That Microsoft tenant is already connected to another Company. Use the Company that already completed consent, or disconnect it first. -
Mailbox setup returns
424: The mapping remains retryable and Resource health isFAILEDwhen the initial sync was attempted. Correct Microsoft permission, mailbox, or provider availability, then send the same request again. -
Mailbox setup returns
429: Microsoft Graph throttled the request. Wait for the response'sRetry-Afternumber of seconds, then retry the same email. -
External events missing: Confirm the resource chosen read calendars via
/selectand that the user granted read permissions. TriggercreateUnavailabilityBulkfor immediate ingestion if needed. -
syncExternal=truestill shows old external blocks: A skipped provider sync does not delete previously stored busy blocks. Reauthorize the calendar when OAuth tokens expire, then retry the availability request or runcreateUnavailabilityBulkfor the affected range. -
Same event on two calendars: Two read calendars on one Resource can each store a busy row for the same provider event ID. That is expected.
-
Leftover Google events after a write-calendar switch: Some older Google events cannot be located after historical calendar switches. Those remote events stay; clean them in Google if needed. New bookings after the switch still write to the current write calendar.
External calendar syncing keeps OnSched availability accurate without revealing internal scheduling rules, while still respecting provider preferences.
Updated 8 days ago
