Skip to main content

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

FieldWhat it controls
typeDiscriminator. Must be the literal GoogleCalendarEventBookingNotification or MicrosoftCalendarEventBookingNotification.
calendarIdThe TimeTime id of the target calendar. Opaque string — fetch from GET /v1/methirdPartyCalendars[].id and use verbatim.
overlapModeWhat to do when multiple bookings land on the same slot. See Overlap modes.
addOrganizerAsAttendeeWhen 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.
addAttendeesWhen 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).
guestsCanSeeOtherGuestsWhen 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.
extraEmailAttendeesAdditional 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.

ValueWhat 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_EVENTSOne 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:

  1. Calendars on the preferred provider for the event type's location (GoogleMeetLocation → Google, MicrosoftOutlookLocation → Microsoft).
  2. Direct entries on the Event Type (notifications.calendars[]) before indirected refs (notifications.calendarsFromResources[]) before linked-resource contributions.
  3. 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 anotherguestsCanSeeOtherGuests: 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 listaddOrganizerAsAttendee: 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's location. For GoogleMeetLocation, MicrosoftOutlookLocation, or an online BookerSelectionLocation answer, a conference is created. For offline FixedLocation (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. guestNotificationsMode is accepted on the Microsoft variant for API symmetry but ignored. To suppress notifications, use addAttendees: false.
  • guestsCanSeeOtherGuests: false is implemented by setting Graph's event.hideAttendees: true.
  • No equivalent of Google's colorId; the field is absent from the Microsoft variant.

Google Calendar

  • colorId values are documented in Google's colors.get API.
  • guestNotificationsMode = EXTERNAL_ONLY only 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.