How to Integrate Multiple Calendar Services: Architecture Guide for SaaS
Build real-time calendar sync with no data storage. Hands-on code, webhook handling, and OAuth patterns for Google and Outlook via a unified pass-through API.
If you need your B2B SaaS product to read and write events across Google Calendar, Microsoft Outlook, and Calendly without turning your engineering team into a calendar infrastructure team, you have three real options: build each integration by hand, adopt a sync-engine vendor that stores your users' calendar data, or use a real-time pass-through unified API. This guide breaks down the architectural trade-offs of each approach so you can pick the one that matches your compliance posture, engineering capacity, and timeline.
Why Integrating Multiple Calendar Services Is a SaaS Dealbreaker in 2026
Calendar integration is table stakes for any B2B SaaS product that touches scheduling, sales, recruiting, or customer success. If your product cannot sync with the calendar providers your customers already use, your onboarding flow dies on contact.
The market pressure is real. The global appointment scheduling software market was valued at USD 298.11 billion in 2024 and is projected to reach USD 471.58 billion by 2032, growing at a CAGR of 5.90% according to Data Bridge Market Research. That is the broader scheduling ecosystem your product needs to plug into — and it is dominated by two calendar providers that control the vast majority of enterprise inboxes: Google Workspace and Microsoft 365.
Your enterprise customers do not care about the technical difficulty. They expect native calendar sync to work on day one. As we've seen with integrations that unblock enterprise deals, sales reps spend only about 30% of their time actively selling, while the rest goes to administrative tasks, data entry, and internal meetings. Every minute your product forces a rep to manually coordinate a meeting, switch tabs to check availability, or copy-paste a meeting link is a minute your competitor's product does not waste.
Every time a user leaves your application to cross-reference their availability in a separate tab, you lose product stickiness. Traditional on-call and scheduling tools create a "coordination tax" that costs teams roughly 10 to 15 minutes per incident or meeting setup before any actual work begins. Calendar integration is not a feature request — it is a retention mechanism. If your platform does not automate this workflow, your churn rate will reflect that friction.
The Hidden Engineering Costs of Building Calendar Integrations In-House
The initial pitch always sounds manageable: "We just need to read events and create meetings. Two APIs. A couple of weeks, max." Here is why that estimate is wrong by an order of magnitude—a classic example of the "just a few API calls" trap we've covered in our build vs. buy analysis.
Industry estimates put custom calendar app development at $30,000 to $150,000+. A basic app might take 500–700 hours to develop, a medium-complexity project up to 1,000 hours, and a complex build over 1,200 hours. For a multi-provider calendar integration layer with proper RRULE handling, webhook processing, and OAuth lifecycle management, expect the higher end of that range — and that is just the initial build, not the ongoing maintenance. Here is exactly where those hours go.
Recurring Events and RRULEs Will Break Your Brain
RFC 5545 defines the iCalendar specification, including recurrence rules (RRULEs) — the grammar that describes how events repeat. The RRULE property defines frequency, interval, count, and exceptions for repeating events. That sounds straightforward until you encounter a bi-weekly standup with EXDATE exceptions, timezone-crossing recurrences, and "this and following" modifications.
The real pain starts when you realize Google and Microsoft represent recurrence completely differently. Google Calendar returns a standard RRULE string like RRULE:FREQ=WEEKLY;UNTIL=20261201T000000Z;BYDAY=TU,TH. Microsoft Graph returns a complex JSON object representing the recurrence pattern and range. Your engineering team must write an abstraction layer that parses both formats, calculates the actual instances of each meeting to expand the series, and handles exceptions (like when a user moves just one instance of a recurring meeting to a different day).
It gets worse. Google's official guidance for editing "this and future events" requires two separate API requests that split the original recurring event into two, setting the UNTIL component of the RRULE to point before the start time of the first target instance. A single "edit this and future events" action in the UI requires your backend to split, truncate, and re-create events atomically. This single feature routinely consumes weeks of engineering time.
Timezone and Daylight Saving Time Nightmares
Timezones are not static. The IANA Time Zone Database updates multiple times a year because governments frequently change daylight saving time rules.
When a timezone transitions into or out of daylight savings, repeating events are expected to remain at the same local time — lunch is always scheduled for 12:30, even if the underlying UTC time shifts by an hour. If your application stores a meeting as a static UTC timestamp based on a future local time, and the government changes the DST transition date before the meeting occurs, your stored UTC timestamp is now wrong. You must store the local time and the exact IANA timezone identifier (e.g., America/New_York), then calculate the UTC offset dynamically at runtime.
To make matters worse, Google and Microsoft use different timezone identifier formats. Google uses IANA identifiers. Microsoft uses Windows timezone names. These differences multiply across every CRUD operation.
Microsoft Graph API Has Its Own Landmines
Beyond recurrence and timezones, Microsoft's calendar API has well-documented quirks that will consume entire sprint cycles:
- The Microsoft Graph Calendar API is not supported for on-premises or hybrid Exchange mailboxes. If your customer runs a hybrid Exchange setup, your Graph integration silently fails.
- Room and resource mailboxes do not support subscriptions through webhooks. Your webhook-based sync strategy breaks for conference rooms.
CalendarViewexpands recurring series into individual instances, whileEventsshows the master recurring series object but may not expand all occurrences. This inconsistency behaves differently depending on user context, organizer vs. attendee role, and how the event was stored.
Webhook Handshakes and Expirations
Listening for calendar updates requires webhooks, and every provider handles them differently.
- Google Calendar requires you to register a receiving endpoint, but those subscriptions expire. Your system must run cron jobs to proactively renew the watch channels before they drop.
- Microsoft Graph requires a synchronous validation handshake. When you register a webhook, Microsoft sends a validation token to your endpoint, and your server must echo it back in plain text within a few seconds, or the registration fails.
Building a unified event ingestion pipeline that handles both of these paradigms requires dedicated infrastructure.
3 Architectural Models to Integrate Multiple Calendar Services
There are three fundamental approaches. Each trades off control, speed, and data residency in different ways. (For a deeper treatment of these patterns beyond just calendars, see our breakdown of the 3 integration models.)
Model 1: Direct 1:1 API Integrations
Your team writes a dedicated client for Google Calendar API, another for Microsoft Graph, another for Calendly, and so on. You own the OAuth flows, token refresh, data mapping, pagination, rate-limit backoff, and webhook verification for each provider.
When it makes sense: You are integrating with exactly one or two calendar providers, and calendar is the absolute core of your product (e.g., you are building a scheduling tool).
The painful reality: This model works until you hit 3–5 providers. Then the maintenance burden becomes its own product. Each provider has different webhook formats, different recurrence models, different event schemas. Google uses dateTime with an explicit timeZone property. Microsoft uses dateTimeTimeZone with a nested timeZone field that accepts Windows timezone names, not IANA identifiers. These differences multiply across every CRUD operation.
Model 2: Sync-Engine Calendar APIs (The Data Warehouse Model)
Several vendors — Nylas and Cronofy among them — maintain a persistent sync between your users' calendars and a centralized data store. You query their warehouse instead of the provider directly.
When it makes sense: You need extremely fast reads, offline access to historical calendar data, or full-text search across events.
The Compliance Risk: Sync engines force you to store sensitive customer PII (meeting titles, attendee emails, descriptions) on a third-party server. For enterprise B2B SaaS, explaining to a CISO why their executives' calendar data is sitting in an integration vendor's database is often a dealbreaker that kills the procurement process.
Model 3: Real-Time Pass-Through Unified API
A pass-through unified API does not store calendar data. Every API call from your product is translated on the fly into the appropriate provider-specific request, executed in real time, and the response is normalized back into a common schema before returning to you.
When it makes sense: You need multi-provider calendar support, your customers care about data residency, and you want to avoid running a "calendar sync warehouse" as a compliance liability.
The trade-off: Read latency depends on the underlying provider's API speed. You cannot query across accounts unless you build your own caching layer on top. But for the 80%+ of calendar use cases — checking availability, creating events, listing upcoming meetings — real-time pass-through is fast enough and dramatically simpler to reason about from a compliance standpoint.
flowchart LR
A[Your SaaS Product] -->|Unified API Call| B[Pass-Through<br>Unified API]
B -->|Translated Request| C[Google Calendar API]
B -->|Translated Request| D[Microsoft Graph API]
B -->|Translated Request| E[Calendly API]
C -->|Raw Response| B
D -->|Raw Response| B
E -->|Raw Response| B
B -->|Normalized Response| A| Criteria | Direct Build | Sync Engine | Pass-Through Unified API |
|---|---|---|---|
| Time to first integration | 4–8 weeks | 1–2 weeks | Days |
| Data residency risk | Low (you control it) | High (vendor stores data) | Low (no storage) |
| Read latency | Lowest (direct call) | Lowest (local cache) | Moderate (pass-through) |
| Maintenance burden | High (per provider) | Low (vendor manages) | Low (vendor manages) |
| Schema normalization | You build it | Vendor provides it | Vendor provides it |
| Provider coverage | Only what you build | Vendor's catalog | Vendor's catalog |
How Truto's Unified Calendar API Solves the "Sync Warehouse" Problem
Truto takes the pass-through approach. Every call to Truto's Unified Calendar API is a real-time proxy to the target calendar provider. No calendar data is stored, cached, or replicated on Truto's infrastructure.
The architecture separates three concerns cleanly:
- The unified interface — a common schema for events, calendars, availability, contacts, and attachments that your engineering team codes against once.
- The proxy layer — handles the actual HTTP call to Google, Microsoft, Calendly, or whichever provider is behind a given connected account.
- The mapping layer — declarative JSONata configurations (not code) that translate between the unified schema and each provider's native format at runtime, including field names, date formats, pagination styles, and query parameters.
This means no integration-specific code runs at execution time. A request to list events for a Google Calendar account and the same request for an Outlook account follow the exact same code path — only the declarative mapping configuration differs. You can read more about why this approach is necessary in our guide on schema normalization.
The practical upside: you ship one integration to Truto's Calendar API, and your product works across every supported calendar provider. Adding a new provider is a configuration change on Truto's side, not a code change on yours.
Use Cases Enabled by the Unified Schema
This architecture supports both traditional SaaS workflows and modern AI agent use cases:
- Autonomous Meeting Orchestration: An AI agent receives an email request for a meeting, uses the
/availabilityendpoint to find open slots for the internal team, replies with options, and upon confirmation, uses the/eventsendpoint to secure the time and invite the external contacts. - Pre-Meeting Context Briefings (RAG): A cron job triggers an agent 15 minutes before an event. The agent fetches the event details, cross-references the attendees in a Unified CRM API, and sends a synthesized briefing directly to the user.
- Smart Time-Blocking: An application monitors a user's task list in a ticketing system. It queries their availability and automatically creates "Deep Work" events on their calendar to ensure complex tasks have dedicated focus time.
For AI Agent Builders: Truto natively supports the Model Context Protocol (MCP). Instead of manually wiring REST endpoints into your agent architecture, you can generate ready-to-use tools directly from the integration config, giving your agents immediate, secure access to user calendars. See our deep dive on the unified calendar API for AI agents.
Quickstart: Real-Time Calendar Pass-Through in 10 Minutes
This section gets you from zero to reading and writing calendar events through a real-time pass-through API with no data storage. Every request is proxied directly to the provider - no calendar data is persisted, cached, or replicated on the integration layer.
Prerequisites: A Truto account, an API key, and at least one connected calendar account (Google or Outlook) with an integrated_account_id.
Fetch Events (curl)
curl -X GET "https://api.truto.one/unified/calendar/events?integrated_account_id=acc_01H...&start=2026-03-23T00:00:00Z&end=2026-03-30T00:00:00Z&limit=20" \
-H "Authorization: Bearer YOUR_TRUTO_API_KEY"Sample response (identical shape whether the account is Google Calendar, Outlook, or Calendly):
{
"result": [
{
"id": "evt_abc123",
"title": "Q3 Product Review",
"description": "Reviewing the updated roadmap.",
"start_time": "2026-03-25T14:00:00Z",
"end_time": "2026-03-25T14:30:00Z",
"timezone": "America/New_York",
"status": "confirmed",
"attendees": [
{ "email": "sales.rep@yourcompany.com", "response_status": "accepted" },
{ "email": "client@external.com", "response_status": "needs_action" }
],
"meeting_link": "https://meet.google.com/abc-defg-hij",
"is_recurring": false,
"calendar_id": "cal_primary",
"remote_data": { "kind": "calendar#event" }
}
],
"next_cursor": "eyJwYWdlIjoy...",
"prev_cursor": null
}The remote_data field preserves the raw provider response for any fields not covered by the unified schema. This is the same structure you get from every supported provider - the mapping layer handles the translation at request time.
Check Availability (curl)
curl -X POST "https://api.truto.one/unified/calendar/availability?integrated_account_id=acc_01H..." \
-H "Authorization: Bearer YOUR_TRUTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"time_min": "2026-03-25T09:00:00Z",
"time_max": "2026-03-25T17:00:00Z",
"attendees": [
{ "email": "sales.rep@yourcompany.com" }
]
}'Sample response:
{
"result": [
{
"email": "sales.rep@yourcompany.com",
"busy": [
{ "start": "2026-03-25T10:00:00Z", "end": "2026-03-25T10:30:00Z" },
{ "start": "2026-03-25T13:00:00Z", "end": "2026-03-25T14:00:00Z" }
]
}
]
}Whether this hits Google's FreeBusy API or Microsoft's getSchedule endpoint is determined entirely by the integrated_account_id. Your code never branches on provider type.
Node.js (Express) - Full Working Example
import express from 'express';
const TRUTO_API_KEY = process.env.TRUTO_API_KEY;
const TRUTO_BASE = 'https://api.truto.one/unified/calendar';
async function trutoFetch(path, options = {}) {
const res = await fetch(`${TRUTO_BASE}${path}`, {
...options,
headers: {
'Authorization': `Bearer ${TRUTO_API_KEY}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
const error = await res.json();
throw new Error(`Truto API error ${res.status}: ${JSON.stringify(error)}`);
}
return res.json();
}
const app = express();
app.use(express.json());
// List events for a connected account
app.get('/api/events', async (req, res) => {
const { integrated_account_id, start, end } = req.query;
const data = await trutoFetch(
`/events?integrated_account_id=${integrated_account_id}&start=${start}&end=${end}`
);
res.json(data);
});
// Check availability across any provider
app.post('/api/availability', async (req, res) => {
const { integrated_account_id, time_min, time_max, attendees } = req.body;
const data = await trutoFetch(
`/availability?integrated_account_id=${integrated_account_id}`,
{
method: 'POST',
body: JSON.stringify({ time_min, time_max, attendees }),
}
);
res.json(data);
});
// Create an event on any connected calendar
app.post('/api/events', async (req, res) => {
const { integrated_account_id, ...eventData } = req.body;
const data = await trutoFetch(
`/events?integrated_account_id=${integrated_account_id}`,
{
method: 'POST',
body: JSON.stringify(eventData),
}
);
res.json(data);
});
app.listen(3000, () => console.log('Running on :3000'));That is the entire integration layer. No provider-specific HTTP clients, no RRULE parsing, no timezone conversion logic. The integrated_account_id routes each request to the correct provider at runtime, and the response always conforms to the same unified schema. Because the API is pass-through, no calendar data touches your integration layer or Truto's infrastructure - it flows directly between your application and the provider.
Handling Authentication and Token Refreshes Across Google and Outlook
OAuth token management is one of those things that sounds trivial and then eats your on-call rotation alive. Google access tokens expire after 1 hour. Microsoft tokens expire after 60–90 minutes by default. If your refresh logic races, fails silently, or does not handle revoked consent, your users see "disconnected" errors at the worst possible time — right before their important meeting.
Here is how Truto handles this so your team does not have to:
- Proactive token refresh with a buffer window: Before every API call, the system checks whether the token will expire within the next 30 seconds. If it will, a refresh is triggered before the call is made — not after a 401 failure. This eliminates the "first request fails, retry succeeds" pattern that plagues naive implementations.
- Scheduled background refresh: A separate background alarm pre-schedules token refreshes 60–180 seconds before expiry, so tokens are almost always fresh before any API call arrives.
- Automatic reauth detection: When a refresh token is itself revoked or expired (common when users change passwords or admins revoke app consent), the connected account is flagged as needing re-authorization and a webhook event is fired to your application so you can prompt the user.
- Encrypted credential storage: Access tokens, refresh tokens, and API keys are encrypted at rest. They are never exposed in list responses — only used internally when executing API calls.
sequenceDiagram
participant App as Your App
participant Proxy as Unified API Proxy
participant Auth as Credential Manager
participant Provider as Google/Outlook
App->>Proxy: Request (Create Event)
Proxy->>Auth: Check Token Expiry
alt Token expires in < 30s
Auth->>Provider: Exchange Refresh Token
Provider-->>Auth: New Access Token
Auth-->>Proxy: Injects New Token
else Token Valid
Auth-->>Proxy: Injects Existing Token
end
Proxy->>Provider: Execute API Call
Provider-->>Proxy: Native Response
Proxy-->>App: Unified JSON ResponseToken Lifecycle Patterns for Multi-Provider Deployments
When managing hundreds or thousands of connected calendar accounts, token refresh stops being a simple if-else check and becomes a distributed systems problem. Here are the patterns that matter at scale:
Pattern 1: Pre-call validation. Every API call checks token expiry before executing. If the token expires within 30 seconds, a refresh is triggered inline before the request proceeds. This eliminates the "first request fails with 401, retry succeeds" race condition that plagues most direct-build implementations.
Pattern 2: Background scheduling with jitter. A background process schedules token refreshes 60-180 seconds before expiry, with randomized timing to avoid thundering-herd problems. This matters most during bulk onboarding - if 200 users connect their calendars in the same hour, their tokens will all expire around the same time. Without jitter, your refresh traffic spikes hard enough to hit provider rate limits on the token endpoint itself.
Pattern 3: Concurrency-safe refresh. When multiple API calls arrive simultaneously for the same account and the token needs refreshing, only one refresh request is sent to the provider. All concurrent callers wait for and share the result of that single refresh. Without this serialization, you burn through your token endpoint quota and risk inconsistent credential state.
Pattern 4: Graceful degradation on revocation. When a refresh token is revoked (user changed password, admin revoked consent, or Google's 7-day expiry for apps still in "Testing" OAuth consent status), the account transitions to a needs_reauth state. Two webhook events drive the lifecycle:
integrated_account:authentication_error- fires when the account is marked as needing re-authorizationintegrated_account:reactivated- fires when a previously broken account successfully re-authenticates
// Handling auth-status webhooks in your app
app.post('/webhooks/truto', async (req, res) => {
const { event_type, payload } = req.body;
if (event_type === 'integrated_account:authentication_error') {
// Prompt the user to reconnect their calendar
await notifyUser(payload.id,
'Your calendar connection has expired. Please reconnect.'
);
}
if (event_type === 'integrated_account:reactivated') {
// Clear any "disconnected" banners in your UI
await clearReauthBanner(payload.id);
}
res.sendStatus(200);
});This is the kind of plumbing that is thankless to build and brutal to get wrong. For a full architectural breakdown, see our post on OAuth at scale and reliable token refreshes.
An honest caveat: No unified API, Truto included, eliminates every edge case. If a customer's Microsoft tenant has hybrid on-premises Exchange mailboxes, the Microsoft Graph API itself does not support those calendars — and no abstraction layer can fix that. What a good unified API does is handle the 95% of OAuth and API lifecycle management that is identical across accounts, so your team only has to deal with the truly exceptional cases.
Step-by-Step: Using a Unified API for Calendar Integration
Implementing calendar features using a unified API dramatically reduces the code footprint in your application. Instead of managing separate HTTP clients for Google and Microsoft, your team interacts with a single RESTful interface. Here is the practical flow.
1. Connect an Account
Your user authenticates with their calendar provider through Truto's hosted OAuth flow. Truto stores the credentials, handles consent scopes, and returns an integrated_account_id that represents that specific connection.
2. Query Availability
Finding free time requires sending a single request to the unified availability endpoint. You pass the email addresses and the time window. The underlying integration — whether Google or Outlook — is determined by the integrated_account_id.
curl -X POST https://api.truto.one/unified/calendar/availability \
-H "Authorization: Bearer YOUR_TRUTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"integrated_account_id": "acc_01H...",
"time_min": "2026-03-25T09:00:00Z",
"time_max": "2026-03-25T17:00:00Z",
"attendees":[
{"email": "sales.rep@yourcompany.com"}
]
}'Truto normalizes the response into a consistent array of free/busy blocks, regardless of how the native provider formats their calendar views.
3. Create an Event
Once a time slot is selected, creating the event uses the same normalized schema. You do not need to worry about the differences between Google's conferenceData object and Microsoft's isOnlineMeeting flag — the unified mapping layer handles the translation.
curl -X POST https://api.truto.one/unified/calendar/events?integrated_account_id=acc_01H... \
-H "Authorization: Bearer YOUR_TRUTO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Q3 Product Review",
"description": "Reviewing the updated roadmap.",
"start_time": "2026-03-25T14:00:00Z",
"end_time": "2026-03-25T14:30:00Z",
"attendees":[
{"email": "client@external.com"}
],
"generate_meeting_link": true
}'Truto translates this into the provider-specific payload — Google's event.insert() call or Microsoft Graph's POST /me/events — and returns the created event in the unified schema.
4. List Upcoming Events
Pull the user's calendar for a date range:
curl -X GET "https://api.truto.one/unified/calendar/events?integrated_account_id=acc_01H...&start=2026-03-23T00:00:00Z&end=2026-03-30T00:00:00Z" \
-H "Authorization: Bearer YOUR_TRUTO_API_KEY"Recurring events are expanded into individual instances, timezone normalization is handled by the mapping layer, and the response shape is identical whether the account is Google, Outlook, or Calendly.
5. React to Changes with Webhooks
Truto receives incoming webhooks from calendar providers, verifies their signatures, maps the raw event payloads into a unified format, and forwards them to your application. Your webhook handler receives the same payload shape for a "meeting updated" event regardless of the source provider.
sequenceDiagram
participant User as Your User
participant App as Your SaaS App
participant Truto as Truto Unified API
participant Provider as Google / Outlook
User->>App: "Schedule a demo"
App->>Truto: GET /availability
Truto->>Provider: Provider-specific free/busy call
Provider-->>Truto: Raw availability data
Truto-->>App: Normalized free/busy slots
App->>User: "Pick a time"
User->>App: Selects 2:00 PM
App->>Truto: POST /events
Truto->>Provider: Provider-specific event creation
Provider-->>Truto: Created event
Truto-->>App: Unified event object
App->>User: "Demo scheduled ✓"This approach is highly effective for teams building AI products, as they can expose this single unified schema to their LLM as a tool, rather than trying to teach the model the intricacies of multiple distinct calendar APIs.
Webhook Handling: Google Watch Channels, Microsoft Validation, and Unified Delivery
Calendar webhooks are where provider differences are most painful to handle directly. Google and Microsoft use fundamentally different subscription models, verification handshakes, and payload formats. If you are building a real-time calendar sync without data storage, you need a webhook pipeline that normalizes all of this into a single delivery contract your application can consume.
What Happens at the Provider Level
Google Calendar push notifications require you to register a watch channel by calling POST /calendars/{calendarId}/events/watch. These channels have a maximum lifetime of about one week, and there is no automatic renewal. Your system must track the expiration timestamp returned in the watch response and proactively create a new channel before the old one lapses. If you miss the window, you stop receiving updates with no error - just silence. To make it harder, Google's push notifications do not include the actual event data in the payload. They signal that something changed on the calendar, and your application must then call the API (typically using a sync token) to fetch what actually changed.
Microsoft Graph subscriptions require a synchronous validation handshake during creation. When you send a POST /subscriptions request, Microsoft immediately sends a request to your notification URL with a validationToken query parameter. Your endpoint must respond within a few seconds by echoing back that exact token as plain text with a 200 status code. If you return it wrapped in JSON, or respond too slowly, the subscription creation fails. Microsoft subscriptions also expire and need periodic renewal.
Calendly webhooks use a simpler model - standard webhook subscriptions with HMAC signature verification - but the event payload format differs entirely from Google and Microsoft.
What Truto Handles Under the Hood
Truto manages the provider-side complexity of webhook ingestion using two patterns:
- Account-specific webhooks: The provider sends events to a URL containing the connected account ID, so the event is immediately routed to the correct account context.
- Environment-level fan-out: Some providers send all events to a single URL for the entire integration. Truto receives the event, evaluates the payload to determine which connected accounts it belongs to, and fans out the normalized event to each.
In both cases, Truto handles the provider-specific verification (Google's sync messages, Microsoft's validation token handshake, Calendly's HMAC signatures), maps the raw event through declarative JSONata expressions into the unified schema, enriches the payload by fetching the full resource from the provider API if the webhook only contained a resource ID, and then delivers the normalized event to your webhook endpoint.
What You Receive: A Unified Webhook Contract
Regardless of which provider triggered the event, your application receives a normalized payload:
{
"event_type": "record:updated",
"payload": {
"resource": "calendar/events",
"records": [
{
"id": "evt_abc123",
"title": "Q3 Product Review",
"start_time": "2026-03-25T14:00:00Z",
"end_time": "2026-03-25T14:30:00Z",
"status": "confirmed",
"attendees": [
{ "email": "client@external.com", "response_status": "accepted" }
],
"remote_data": { "kind": "calendar#event" }
}
],
"integrated_account_id": "acc_01H...",
"raw_event_type": "google.calendar.events.updated",
"raw_payload": {}
}
}The raw_event_type and raw_payload fields preserve the original provider event for cases where you need provider-specific details beyond the unified schema.
Verifying Truto's Outbound Webhook Signatures
Truto signs every outbound webhook with an X-Truto-Signature header using HMAC-SHA256. Always verify this before processing:
import crypto from 'crypto';
function verifyTrutoWebhook(rawBody, signature, secret) {
if (!signature) return false;
const computed = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computed)
);
} catch {
return false;
}
}
// Express setup - capture raw body for accurate signature verification
const app = express();
app.use('/webhooks/truto', express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
app.post('/webhooks/truto', (req, res, next) => {
const sig = req.headers['x-truto-signature'];
if (!verifyTrutoWebhook(req.rawBody, sig, process.env.TRUTO_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
});Use the raw request body for verification. Middleware that parses and re-stringifies JSON can alter whitespace or key ordering, which breaks the HMAC comparison. Capture the raw buffer before parsing, as shown above.
Processing Calendar Webhooks (Express)
app.post('/webhooks/truto', async (req, res) => {
// Always respond quickly - process asynchronously
res.sendStatus(200);
const { event_type, payload } = req.body;
const { resource, records, integrated_account_id } = payload;
if (resource !== 'calendar/events') return;
for (const record of records) {
switch (event_type) {
case 'record:created':
await handleNewEvent(integrated_account_id, record);
break;
case 'record:updated':
await handleUpdatedEvent(integrated_account_id, record);
break;
case 'record:deleted':
await handleDeletedEvent(integrated_account_id, record);
break;
}
}
});Respond first, process later. Both third-party providers and Truto's delivery pipeline expect a fast HTTP response. If your handler takes more than a few seconds, the sender will retry - causing duplicate deliveries. Do your business logic asynchronously after returning the 200.
Retry, Rate-Limit, and Error-Handling Recommendations
Calendar providers enforce rate limits aggressively - both per-user and per-second limits that are much tighter than the aggregate daily quota might suggest. Hitting these limits returns HTTP 429 responses, and how you handle them directly affects reliability.
Retry with Exponential Backoff and Jitter
When Truto receives a 429 or 5xx from a provider, it returns a structured error to your application. Your client-side retry logic should respect the Retry-After header when present and fall back to exponential backoff with jitter:
async function trutoFetchWithRetry(path, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(`https://api.truto.one/unified/calendar${path}`, {
...options,
headers: {
'Authorization': `Bearer ${process.env.TRUTO_API_KEY}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (res.status === 429) {
const retryAfter = res.headers.get('retry-after');
const waitMs = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
if (res.status >= 500 && attempt < maxRetries) {
const waitMs = Math.min(
1000 * Math.pow(2, attempt) + Math.random() * 500, 30000
);
await new Promise(r => setTimeout(r, waitMs));
continue;
}
if (!res.ok) {
const error = await res.json();
throw new Error(`Truto API error ${res.status}: ${JSON.stringify(error)}`);
}
return res.json();
}
throw new Error(`Request failed after ${maxRetries + 1} attempts`);
}Error Categories and Recommended Actions
| HTTP Status | Meaning | Action |
|---|---|---|
| 400 | Bad request (malformed body, missing field) | Fix the request. Do not retry. |
| 401 | Auth error (invalid API key or provider token revocation) | Check your API key. If the error references the provider, the connected account needs re-authorization. |
| 404 | Resource not found (event deleted, calendar removed) | Handle gracefully in your UI. Do not retry. |
| 429 | Rate limited by the underlying provider | Retry with exponential backoff. Respect Retry-After header. |
| 500+ | Server error (Truto or provider) | Retry with exponential backoff, up to 3 attempts. |
Idempotency for Write Operations
Calendar event creation is not inherently idempotent. If you retry a POST /events that actually succeeded but timed out before you received the response, you create a duplicate event. To mitigate this: maintain a client-side log of pending creates and deduplicate by matching on title + time + attendees before retrying. Some providers support idempotency keys natively, but this is not universal across Google, Microsoft, and Calendly.
Troubleshooting Checklist: Webhooks, Recurrence, and Token Issues
Webhook Issues
- Not receiving webhooks after setup? Confirm your endpoint is publicly accessible over HTTPS and responds with a
200status within 5 seconds. Check that you have registered a webhook subscription in Truto with the correct event types. - Receiving duplicate events? This is expected in at-least-once delivery. Make your handler idempotent by tracking event IDs you have already processed.
- Signature verification failing? Ensure you are verifying against the raw request body, not a re-serialized version. JSON parsing and re-stringification can change whitespace or key ordering, which breaks the HMAC.
- Missing events from specific providers? Check that the connected account's OAuth scopes include calendar read access. Google requires
https://www.googleapis.com/auth/calendar.events.readonly(or broader) and Microsoft requiresCalendars.Readat minimum.
Recurrence and Timezone Issues
- Recurring events showing as a single entry? Use the
/eventsendpoint with a date range (startandendparameters). The unified API expands recurring series into individual instances within the queried range. Without a date range, some providers return only the master series object. - Event times off by one hour after DST transitions? This usually means you are storing UTC timestamps calculated from a future local time. Always store and transmit times in ISO 8601 with explicit timezone identifiers, not pre-calculated UTC offsets.
- "This and following" edits creating unexpected results? Google splits the original series into two separate recurring events. The unified API reflects this as distinct events. Your application should handle the case where a recurring event's ID changes after a series split.
Token and Authentication Issues
needs_reauthstatus on a connected account? The user's refresh token was revoked. Common triggers: user changed their Google/Microsoft password, an admin revoked your app's access in the tenant admin console, or Google's 7-day refresh token expiry for apps in "Testing" OAuth consent screen status (move to "Published" for long-lived tokens).- Intermittent 401 errors followed by successful retries? This can happen if your application makes concurrent requests while a token refresh is in progress. Truto serializes concurrent refreshes for the same account to prevent this, but if you are caching tokens on your side, make sure your cache invalidation triggers on 401 responses.
- Microsoft hybrid Exchange accounts returning errors? The Microsoft Graph Calendar API does not support on-premises or hybrid Exchange mailboxes. No API layer can work around this - the limitation is at the provider level. Check with the customer's IT team whether their mailbox is fully migrated to Exchange Online.
What This Means for Your Roadmap
Calendar integration should not be a multi-quarter engineering initiative. The real question is not "how do we integrate calendars" but "how little engineering time can we spend on calendar plumbing while still shipping a reliable experience."
Here is the honest decision framework:
- If calendars are your core product (you are literally a scheduling company), build direct integrations. You need that level of control.
- If you need deep historical search across calendar data, a sync-engine approach might justify the data residency trade-off.
- If you need multi-provider calendar support and want to ship in days, not months, a pass-through unified API is the right call. This is where Truto fits.
The worst decision is building calendar integrations "temporarily" in-house with the plan to clean them up later (a trap we also see frequently with native CRM integrations). That cleanup never happens. Those integrations become the engineering equivalent of load-bearing walls — terrifying to touch, impossible to remove, and slowly accumulating tech debt with every provider API change.
Start with the end state you actually want. For most B2B SaaS products, that means shipping calendar support as fast as possible and getting your engineers back to building your actual product.
FAQ
- How much does it cost to build a custom calendar integration?
- Industry estimates put custom calendar app development at $30,000 to $150,000+, with complex integrations requiring 1,000+ developer hours. This covers only the initial build — ongoing maintenance for API changes, token rotation, and edge cases adds significantly to the total each year.
- What is the hardest part of integrating Google Calendar and Outlook?
- Recurring events (RRULEs), timezone handling across DST boundaries, and OAuth token lifecycle management are the three biggest engineering challenges. Google and Microsoft also use different timezone identifier formats (IANA vs. Windows), different event schemas, and different webhook validation models.
- What is the difference between a sync engine and a pass-through calendar API?
- A sync engine copies and stores your users' calendar data on the vendor's infrastructure for fast reads. A pass-through API makes real-time requests to the calendar provider without storing data, which eliminates data residency concerns but means read latency depends on the provider's API speed.
- What is the fastest way to integrate Google Calendar and Outlook?
- The fastest method is using a unified calendar API. It provides a single normalized schema for querying availability and creating events, eliminating the need to write and maintain separate code for each calendar provider.
- Should I build calendar integrations in-house or use a third-party solution?
- Build in-house only if calendars are the absolute core of your product and you need maximum control. For everyone else, the maintenance burden of OAuth flows, RRULE parsing, webhook verification, and provider-specific quirks across 3+ calendar services makes a buy decision significantly cheaper long-term.