Calendar booking notifications
When a booking is confirmed, TimeTime can materialize it as an event on one or more third-party calendars (Google Calendar or Microsoft Outlook). The shape of that provider event — title, body, attendees, conference link, color, who gets notified — is controlled per calendar by a calendar booking notification.
This guide walks through the available knobs, how to express common use cases (1-on-1, group events, multi-resource fan-out), and the provider-specific quirks worth knowing.
Where notifications live
A CalendarBookingNotification can be attached to three places:
- Event Type (
eventType.notifications.calendars). Notifications declared here fire on every booking of that event type. This is the direct, inline form. - Resource (
resource.bookingNotifications.calendars). When a booking uses that resource as a linked resource (counting towards capacity and availability), the resource's notifications also fire. - Indirected reference (
eventType.notifications.calendarsFromResources). A pointer to a resource whose calendar entries should host bookings of this event type, without the resource counting towards capacity. The resource owns the full notification spec; the ref is just a pointer. See Indirected calendar references.
All three sources are merged at booking time. Notifications targeting the same calendar are deduplicated; if the same calendar id appears in multiple sources, the Event Type's direct entry wins, then the indirected one, then the linked-resource one.
What happens after the merge — fan out to one provider event per calendar,
or coalesce into one shared event — is controlled by
multiCalendarMode.
Anatomy of a notification
There are two variants, picked via the type discriminator. Both share most
fields; only the conference behavior differs.
Google variant
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<google calendar id from /v1/me>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "ALL",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
},
"colorId": null
}
Microsoft variant
{
"type": "MicrosoftCalendarEventBookingNotification",
"calendarId": "<microsoft calendar id from /v1/me>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"extraEmailAttendees": [],
"createTeamsMeetingBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
}
}
Fields, in plain English
| Field | What it controls |
|---|---|
type | Discriminator. Must be the literal GoogleCalendarEventBookingNotification or MicrosoftCalendarEventBookingNotification. |
calendarId | The TimeTime id of the target calendar. Opaque string — fetch from GET /v1/me → thirdPartyCalendars[].id and use verbatim. |
overlapMode | What to do when multiple bookings land on the same slot. See Overlap modes. |
addOrganizerAsAttendee | When false, the organizer's email is omitted from the attendee list. They still own the event. Useful when the calendar owner is a scheduler booking on behalf of others. |
addAttendees | When false, only the organizer ends up on the provider event — every other participant is dropped. This is also the lever to suppress attendee notification emails on Microsoft (Graph has no per-call equivalent of Google's sendUpdates). |
guestsCanSeeOtherGuests | When false, attendees other than the organizer cannot see one another in the event details. Maps to Google's guestsCanSeeOtherGuests (1:1) and to the inverse of Microsoft's event.hideAttendees. |
guestNotificationsMode (Google only) | ALL / EXTERNAL_ONLY / NONE. Routed to Google's sendUpdates query parameter on insert + attendee patches. |
extraEmailAttendees | Additional email addresses always added as attendees to the provider event (assistants, observers, managers cc'd on every booking). |
createGoogleMeetBehavior (Google only) / createTeamsMeetingBehavior (MS only) | Whether and how a Meet/Teams conference is attached. See Conferences. |
colorId (Google only) | Optional Google colorId for the event. null keeps the default color. Microsoft Graph uses categories instead and ignores this field. |
Overlap modes
overlapMode determines what happens when two or more bookings land on the
same slot ((eventTypeId, start, end, calendarId)). This is only possible when
the event type's maxConcurrentBookings > 1.
NEW_EVENT (default)
Each booking creates its own provider event. The events are independent — separate ids, separate titles, separate conference links (Meet/Teams), separate attendee lists.
Use this for 1-on-1 services where each booking is private to its booker.
ADD_NEW_ATTENDEES_TO_EXISTING_EVENT
The slot's first booking creates the provider event. Subsequent bookings on the same slot don't create new events — instead, their attendees are merged into the existing one. All bookings end up sharing a single provider event and a single conference link.
Use this for group events: webinars, group classes, group interviews where candidates shouldn't each get their own meeting URL.
What changes for shared events
Because one provider event backs multiple bookings, TimeTime adjusts how the event is written so the first booker's identity doesn't leak to later attendees:
- The title is just the event-type name (e.g.
"Group Interview"), not"<first-booker> - Group Interview". - The body keeps the generic info (event-type description, location, online conference link) but omits answered-question values, booker notes, and the per-booking management link.
If you also want attendees to be hidden from each other, set
guestsCanSeeOtherGuests: false — see the next section.
Multiple calendars on one booking
When a single booking ends up with more than one calendar configured to receive
it — multiple entries in notifications.calendars, a linked resource that
carries its own calendar, an indirected reference, or any combination — TimeTime
has to decide whether to fan out (one provider event per calendar) or coalesce
(one shared event with the other calendars represented as attendees). That
choice is notifications.multiCalendarMode.
| Value | What happens with N contributing calendars |
|---|---|
COALESCE_INTO_FIRST_CALENDAR (default) | One provider event is created on the first calendar. The OAuth account email of every other contributing calendar is added as an extra attendee on that event. Those owners see the booking on their own calendar via the provider's native attendee-invite mechanism. |
INDEPENDENT_EVENTS | One independent provider event is created on each contributing calendar. The booker is invited on every event; each calendar has its own conference link unless reuseOnlineConferenceIfPresent is on. Use this when each calendar genuinely needs its own entry (a tutor's calendar + a room's calendar both must show "busy", for example). |
When only one calendar contributes, both modes produce the same result — there's nothing to coalesce.
Why coalesce by default
Multi-calendar bookings most often model a single human-facing meeting: a panel
interview, a multi-host event, a shared team calendar. Customers expect one
calendar event, one conference link, and one invitation in the booker's inbox
— not N. COALESCE_INTO_FIRST_CALENDAR honors that; INDEPENDENT_EVENTS is
the opt-out for the genuine "each calendar wants its own entry" case.
Picking the anchor
Under COALESCE_INTO_FIRST_CALENDAR, the first calendar in the merged list
hosts the provider event. Order is, in priority:
- Calendars on the preferred provider for the event type's location
(
GoogleMeetLocation→ Google,MicrosoftOutlookLocation→ Microsoft). - Direct entries on the Event Type (
notifications.calendars[]) before indirected refs (notifications.calendarsFromResources[]) before linked-resource contributions. - Within
calendars[], the configured array order.
In practice that means: put the calendar you want as the anchor (the recruiter's
inbox, the team shared calendar, the booker's primary) first in
notifications.calendars, and the rest of the contributing calendars ride as
attendees.
Backward compatibility
Customers still on the legacy thirdPartyCalendars field (no
notifications.calendars configured) keep the previous fan-out behavior
unconditionally — multiCalendarMode only takes effect when you've opted into
notifications.calendars or notifications.calendarsFromResources.
Privacy & notifications
| You want… | Set… |
|---|---|
| Attendees not to see one another | guestsCanSeeOtherGuests: false |
| No notification emails to attendees (Google) | guestNotificationsMode: "NONE" |
| No notification emails to attendees (Microsoft) | addAttendees: false (MS Graph has no per-call sendUpdates) |
| Organizer not visible in the attendee list | addOrganizerAsAttendee: false |
| Always-on observers (managers, assistants) | extraEmailAttendees: ["manager@acme.com"] |
Conferences
The createGoogleMeetBehavior.mode / createTeamsMeetingBehavior.mode field
controls when the provider creates an online conference link attached to the
event:
ALWAYS— every booking gets a Meet/Teams link, even in-person bookings.NEVER— never request one. The location's joining URL (if any) is preserved as the event location.IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE— default. Defers to the event type'slocation. ForGoogleMeetLocation,MicrosoftOutlookLocation, or an onlineBookerSelectionLocationanswer, a conference is created. For offlineFixedLocation(e.g. "Conference Room A") it isn't.
reuseOnlineConferenceIfPresent (true by default): when the booking already
carries a conference link from a prior sync target, the adapter reuses that
link instead of creating a new one. This guarantees one conference per
booking across multiple notification targets — without it, a booking with
notifications on both a Google and a Microsoft calendar could end up with two
distinct meeting links.
Picking a calendar id
Calendar ids are stable opaque strings. Don't try to parse, derive, or construct them — fetch from the API and use the value verbatim.
curl -H "Authorization: Bearer $TT_API_KEY" https://api.timetime.in/v1/me
The response contains a thirdPartyCalendars array; each entry has an id plus
human-readable fields (provider, account, name, primary) so you can
identify the calendar you want:
{
"thirdPartyCalendars": [
{
"id": "<opaque calendar id>",
"provider": "MICROSOFT",
"account": "user@example.com",
"name": "Calendar",
"primary": true,
"readOnly": false
}
]
}
Copy the id and use it as the calendarId on the notification.
Indirected calendar references
Beyond direct entries on the Event Type or on linked resources, there's a
third way to bring a calendar into a booking: a calendarsFromResources ref.
A ref is a minimal pointer — just a resource id:
{
"notifications": {
"calendarsFromResources": [
{ "resourceId": "<resource id>" }
]
}
}
When a booking is confirmed, TimeTime resolves each ref by reading the
referenced resource's bookingNotifications.calendars verbatim and merging
those entries into the booking's notification list. The resource owns the
full notification spec — calendar id, overlap mode, attendee policy,
conference behavior, extra attendees. Editing the resource once propagates to
every Event Type that references it on the next booking, with no per-Event-Type
config to keep in sync.
When to use which
| You want… | Use… |
|---|---|
| A literal calendar id, inline on the Event Type, never to change. | notifications.calendars[] |
| A specific resource's calendar to host bookings, and the resource to count towards capacity / availability. | linkedResources |
| A specific resource's calendar to host bookings, without the resource consuming capacity (shared team calendars, recruiter inbox, etc.). | notifications.calendarsFromResources[] |
Authorization
Every resource id in calendarsFromResources must be readable by the principal
making the PUT request. References to a resource the principal doesn't own —
and references to resource ids that don't exist — are both rejected with
403 Forbidden. The whole PUT is rejected on the first inaccessible ref (it's
not silently filtered) so it's clear what's wrong.
This means you can't point an Event Type at a colleague's resource unless they share it with you first. The two failure modes ("not yours" and "doesn't exist") look identical from the response — server-side logs capture the specific resource id for the team's debugging.
Worked example: group interview with a shared recruiting calendar
A recruiting team uses a single Google calendar (scheduling@example.com) for
all interview slots, modeled as a TimeTime resource so the OAuth credentials,
the always-attached panellist emails, and the meeting behavior can be edited in
one place. The Event Type's availability comes from an availabilitySchedule
(not from a resource), so the recruiting calendar must not count towards
capacity.
PUT /v1/resources/recruiting-shared:
{
"name": "Recruiting — shared",
"bookingNotifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<scheduling@example.com calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "ALL",
"extraEmailAttendees": ["alex@example.com", "antonio@example.com"],
"createGoogleMeetBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
PUT /v1/event-types/group-interview-A:
{
"name": "Group interview A",
"duration": "PT1H",
"step": "PT1H",
"maxConcurrentBookings": 1,
"availabilitySchedule": { "...": "..." },
"notifications": {
"calendarsFromResources": [
{ "resourceId": "recruiting-shared" }
]
}
}
Every booking lands on scheduling@example.com with Alex and Antonio added as
attendees on every event. Six months later when the team rotates and wants
taylor@example.com cc'd, the Event Type stays untouched — only the
recruiting-shared resource is edited.
Falling back to a direct entry
If an Event Type ever needs the same target calendar but with a different spec
from what the resource declares (different attendees, different overlap mode,
different conference behavior), put a direct calendars[] entry with the
literal calendar id alongside the ref. The direct entry takes precedence in the
merged list. calendarsFromResources is for "follow the resource exactly";
direct entries are for divergence.
Calendar-less resources
If the referenced resource has no bookingNotifications.calendars configured
(brand new, OAuth disconnected, mid-setup), the ref contributes nothing. The
booking still completes; downstream provider dispatch is short-circuited only
when the merged list ends up empty. A missing resource id (deleted between PUT
and booking) is logged at WARN and likewise contributes nothing.
Worked examples
1-on-1 sales call with Google Meet
{
"name": "30-min discovery call",
"duration": "PT30M",
"step": "PT30M",
"maxConcurrentBookings": 1,
"location": { "type": "GoogleMeetLocation" },
"notifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<sales google calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "ALL",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
Group interview with Teams (multiple bookings, one event)
{
"name": "Group interview — backend hires",
"duration": "PT1H",
"step": "PT1H",
"maxConcurrentBookings": 8,
"location": { "type": "MicrosoftOutlookLocation" },
"notifications": {
"calendars": [
{
"type": "MicrosoftCalendarEventBookingNotification",
"calendarId": "<recruiter microsoft calendar id>",
"overlapMode": "ADD_NEW_ATTENDEES_TO_EXISTING_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": false,
"extraEmailAttendees": [],
"createTeamsMeetingBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
What happens: every candidate booking the same slot lands on the same
Outlook event, with the same Teams meeting link, but they can't see each
other in the attendee list. The event title is just "Group interview — backend hires", with no candidate emails.
Panel interview — recruiter anchors, interviewers as attendees
Three interviewers (Alice, Bob, Carol), each on their own Google account, all
need the booking on their calendar; the recruiter's calendar should be the
"home" of the event. With the default COALESCE_INTO_FIRST_CALENDAR, the
booking produces one provider event on the recruiter's calendar with all
three interviewers as attendees — they each see the event on their own
calendar via Google's native invite mechanism.
Resources for the three interviewers (each PUT separately):
{
"name": "Alice",
"bookingNotifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<alice google calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": false,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "NONE",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "NEVER",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
Event Type — direct entry for the recruiter (the anchor), indirected refs for the three interviewers:
{
"name": "Backend panel interview",
"duration": "PT1H",
"step": "PT1H",
"maxConcurrentBookings": 1,
"location": { "type": "GoogleMeetLocation" },
"linkedResources": ["alice", "bob", "carol"],
"notifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<recruiter google calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "ALL",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "IF_EVENT_NEEDS_AN_ONLINE_CONFERENCE",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
The candidate receives one invitation — the recruiter's event — with the Meet link. Alice, Bob, and Carol each receive a Google invite to the same event and see it on their own calendar. If the recruiting team needs to swap an interviewer or reauth a Google account, that's a single resource edit; the Event Type stays untouched.
Variant — recruiter is just a shared inbox, not a person: replace the direct
calendars[] entry with a calendarsFromResources ref pointing at a
recruiting-shared resource (as shown earlier in
Indirected calendar references). The
behavior is identical from the candidate's perspective; the difference is who
manages the recruiter calendar config.
Tutor + room, each on its own calendar
When an event type uses resources, every resource that participates in the
booking contributes its own notifications. Below: a tutoring session needs both
a tutor and a room; each is a resource with its own calendar, and the booking
needs to fan out to both. This is the canonical case for
multiCalendarMode: INDEPENDENT_EVENTS — the room and the tutor must each show
the booking on their own calendar; coalescing them into one event with the
other as an attendee would not communicate the same information.
PUT /v1/resources/tutor-alice:
{
"name": "Alice",
"bookingNotifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<alice google calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": true,
"guestsCanSeeOtherGuests": true,
"guestNotificationsMode": "ALL",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "NEVER",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
PUT /v1/resources/room-101:
{
"name": "Room 101",
"bookingNotifications": {
"calendars": [
{
"type": "GoogleCalendarEventBookingNotification",
"calendarId": "<room-101 google calendar id>",
"overlapMode": "NEW_EVENT",
"addOrganizerAsAttendee": true,
"addAttendees": false,
"guestsCanSeeOtherGuests": false,
"guestNotificationsMode": "NONE",
"extraEmailAttendees": [],
"createGoogleMeetBehavior": {
"mode": "NEVER",
"reuseOnlineConferenceIfPresent": true
}
}
]
}
}
The Event Type linking these two resources opts into independent events:
{
"name": "1h tutoring session",
"linkedResources": ["tutor-alice", "room-101"],
"notifications": {
"multiCalendarMode": "INDEPENDENT_EVENTS"
}
}
When a booking uses both resources, two provider events are created — one on Alice's calendar (with attendees and notification emails) and one on Room 101's calendar (silent, just a placeholder so the room shows as busy). If the event type also declares a notification on the booker's calendar, that's a third target — all three are merged and deduped at dispatch time.
Without the INDEPENDENT_EVENTS opt-in, the default
COALESCE_INTO_FIRST_CALENDAR would produce a single event on Alice's
calendar with room-101@… listed as an attendee — wrong for this scenario
because the room calendar wouldn't show its own busy block in the way most
rooms-as-calendar setups expect.
Provider-specific quirks
Microsoft Graph
- No per-call
sendUpdates.guestNotificationsModeis accepted on the Microsoft variant for API symmetry but ignored. To suppress notifications, useaddAttendees: false. guestsCanSeeOtherGuests: falseis implemented by setting Graph'sevent.hideAttendees: true.- No equivalent of Google's
colorId; the field is absent from the Microsoft variant.
Google Calendar
colorIdvalues are documented in Google's colors.get API.guestNotificationsMode = EXTERNAL_ONLYonly sends emails to attendees outside the calendar's owning domain.
Backwards compatibility
The legacy eventType.thirdPartyCalendars field is still honored when both
notifications.calendars and notifications.calendarsFromResources are empty
or absent. The moment either of those is populated, the canonical path takes
over and the legacy field is ignored for that event type.
Event types still on the legacy field are not affected by
multiCalendarMode — they keep the previous fan-out semantics
unconditionally. The default flip to COALESCE_INTO_FIRST_CALENDAR only kicks
in for event types that have opted into notifications.calendars or
notifications.calendarsFromResources.
The legacy field is marked deprecated in the API spec; new integrations should
use notifications.calendars (with calendarsFromResources for indirection
when you need it) exclusively.
Reference
Full JSON schema and inline examples in the
OpenAPI reference
under BookingNotifications, CalendarBookingNotification,
GoogleCalendarEventBookingNotification,
MicrosoftCalendarEventBookingNotification, and CalendarFromResourceRef.