Skip to content

feat(webhooks): add support for webhooks from multiple calendars#5787

Open
rbwest wants to merge 5 commits intomasterfrom
ryan/nan-5160-contio-add-support-for-webhooks-from-multiple-calendars
Open

feat(webhooks): add support for webhooks from multiple calendars#5787
rbwest wants to merge 5 commits intomasterfrom
ryan/nan-5160-contio-add-support-for-webhooks-from-multiple-calendars

Conversation

@rbwest
Copy link
Copy Markdown
Contributor

@rbwest rbwest commented Apr 3, 2026

Contio requires Google Calendar webhooks from multiple calendars per connection, not just the primary calendar. This PR updates the webhook routing and associated docs to make this possible. It also adds the same routing support to google integration since Contio uses both google-calendar and google integrations for calendar syncing.

NAN-5160: Contio - Add support for webhooks from multiple calendars

  1. Create new google calendar connection
  2. Update connection metadata as described in updated docs with appropriate googleCalendarWatchResourceUris
  3. Subscribe to push notifications for those calendars as outlined in current docs
  4. Create event on multiple calendars and see appropriate connections associated with each webhook

This PR also formalizes the connection-matching model by prioritizing raw x-goog-resource-uri metadata matching for multi-calendar routing while preserving backward-compatible behavior for existing primary-calendar flows, and clarifies webhook behavior for unmatched and multi-match scenarios so integration behavior is more predictable across both Google integrations.


This summary was automatically generated by @propel-code-bot

@linear
Copy link
Copy Markdown

linear bot commented Apr 3, 2026

@mintlify
Copy link
Copy Markdown
Contributor

mintlify bot commented Apr 3, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
nango 🟢 Ready View Preview Apr 3, 2026, 2:17 PM

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

propel-code-bot[bot]

This comment was marked as outdated.


### Multiple calendars

If you want to subscribe to notifications from multiple calendars, add the resource URIs for each calendar to **`metadata.googleCalendarWatchResourceUris`**:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple calendars or any other calendar that isn't the primary, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to know which calendars we have access at the connection post script event? Would we be able to resolve them to store them in the connection config as well? (OOS for this PR, but its something to consider when thinking about the webhooks refactor)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple calendars or any other calendar that isn't the primary, right?

No, it's exclusive. You either match by the listed googleCalendarWatchResourceUris, or we will follow the previous path and assume you want the primary calendar. It's possible for Contio's use case that someone doesn't actually want to subscribe to their primary calendar, so you must explicitly list the URI for each calendar you want to do the routing for.

Are we able to know which calendars we have access at the connection post script event? Would we be able to resolve them to store them in the connection config as well? (OOS for this PR, but its something to consider when thinking about the webhooks refactor)

So the google-calendar connection has access to all the calendars after connection, but we don't know which ones they want to subscribe to notifications for. Specifically for Contio, the user selects this in their application.

  1. User connects to google-calendar or google
  2. Contio fetches a list of all calendars
  3. User selects which calendars from lists they want to sync

So blindly adding all calendars I think is the wrong call and just adds noise, but happy to discuss more.

```
This will associate both listed calendar resource URIs with the connection. Nango will match any incoming webhook with those exact `X-Goog-Resource-URI` values to this connection.

To do this in bulk, iterate over the [list connections](/reference/api/connection/list) response and [update the metadata](/reference/api/connection/update-metadata) for each one.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this line, it really depends on use-case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following. What do you mean?

_No pre-built syncs or actions available yet._

<Tip>Not seeing the integration you need? [Build your own](/guides/primitives/functions) independently.</Tip>
<Tip>Not seeing the integration you need? [Build your own](/guides/primitives/functions) independently.</Tip> No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm blind, but whats the change here? Also, does something change for salesforce-cc ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea. I'm not super familiar with how these auto generated snippets are created. I assumed it was best practice to just commit them, but maybe not?

What should I be doing in these cases?

await GoogleCalendarWebhookRouting.default(nangoMock as unknown as InternalNango, headers as any, {}, '');

expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of just matching whether a mock has been called deep inside with an argument, how feasible is it to set up a test that actually has multiple connections with metadata and see what the result of the routing is?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, let me take a look. I was just following the established pattern of unit tests for simplicity. Is this a blocker?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, I’ll approve to unblock since it’s following an existing pattern but IMO it would be a worthy addition :)

response_path: items
docs: https://nango.dev/docs/api-integrations/google
setup_guide_url: https://nango.dev/docs/api-integrations/google/how-to-register-your-own-google-api-oauth-app
webhook_routing_script: googleWebhookRouting
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We marked other google apis as now supporting webhooks (google-ads, google-analytics, google-bigquery, google-chat, etc) but we haven't added the routing for them? Do webhooks for those only work when connected through Google Platform? Or the docs update is wrong?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, sorry, I didn't take a close look at the auto generated updates. It will only work for google or google-calendar. How do I prevent the auto generated updates?

Co-authored-by: Agustin Ayerza <agusayerza@gmail.com>
Copy link
Copy Markdown
Contributor

@propel-code-bot propel-code-bot bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid multi-calendar webhook enhancement, but docs consistency and one missing edge-case test should be addressed before merge.

Status: Changes Suggested | Risk: Medium

Issues Identified & Suggestions
  • Align URI encoding in docs example with actual Google header format: docs/api-integrations/google-calendar/webhooks.mdx
  • Add test for missing x-goog-resource-uri fallback behavior: packages/server/lib/webhook/google-calendar-webhook-routing.unit.test.ts
Review Details

📁 28 files reviewed | 💬 2 comments

Instruction Files
└── .claude/
    ├── agents/
    │   └── nango-docs-migrator.md
    └── skills

👍 / 👎 individual comments to help improve reviews for you

}
}'
```
This will associate both listed calendar resource URIs with the connection. Nango will match any incoming webhook with those exact `X-Goog-Resource-URI` values to this connection.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

[Documentation] The docs example sets googleCalendarWatchResourceUris to URIs like https://www.googleapis.com/calendar/v3/calendars/cal1@example.com/events?alt=json (unencoded @). However, the docs earlier state "Store the X-Goog-Resource-URI string exactly as Google sends it: same encoding, path, and query string," and the test fixture uses user%40example.com (percent-encoded). The example curl payload uses unencoded @ in the email addresses, which would fail to match Google's actual X-Goog-Resource-URI header (which uses %40). Update the example to use percent-encoded email addresses consistent with the real header format:

Change:

"https://www.googleapis.com/calendar/v3/calendars/cal1@example.com/events?alt=json",
"https://www.googleapis.com/calendar/v3/calendars/cal2@example.com/events?alt=json"

To:

"https://www.googleapis.com/calendar/v3/calendars/cal1%40example.com/events?alt=json",
"https://www.googleapis.com/calendar/v3/calendars/cal2%40example.com/events?alt=json"
Context for Agents
The docs example sets `googleCalendarWatchResourceUris` to URIs like `https://www.googleapis.com/calendar/v3/calendars/cal1@example.com/events?alt=json` (unencoded `@`). However, the docs earlier state "Store the `X-Goog-Resource-URI` string exactly as Google sends it: same encoding, path, and query string," and the test fixture uses `user%40example.com` (percent-encoded). The example curl payload uses unencoded `@` in the email addresses, which would fail to match Google's actual `X-Goog-Resource-URI` header (which uses `%40`). Update the example to use percent-encoded email addresses consistent with the real header format:

Change:
```
"https://www.googleapis.com/calendar/v3/calendars/cal1@example.com/events?alt=json",
"https://www.googleapis.com/calendar/v3/calendars/cal2@example.com/events?alt=json"
```
To:
```
"https://www.googleapis.com/calendar/v3/calendars/cal1%40example.com/events?alt=json",
"https://www.googleapis.com/calendar/v3/calendars/cal2%40example.com/events?alt=json"
```

File: docs/api-integrations/google-calendar/webhooks.mdx
Line: 320

await GoogleCalendarWebhookRouting.default(nangoMock as unknown as InternalNango, headers as any, {}, '');

expect(mock).toHaveBeenCalledTimes(3);
expect(mock).toHaveBeenCalledTimes(4);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended

[Testing] The fallback chain test (tries metadata.googleCalendarWatchResourceUris first...) asserts toHaveBeenCalledTimes(4) and hard-codes all four call orderings, but the mock is set up at the describe-suite level — specifically, nangoMock.executeScriptForWebhooks = mock replaces the method on the instance right before the call. However, each it block creates its own nangoMock, so the mock from the first test cannot leak into subsequent ones. That's fine. What IS missing is a test for the case where x-goog-resource-uri is absent from a google-calendar webhook: the function will still try all three email-based fallbacks with an undefined connectionIdentifierValue. Add a test asserting the behavior when resourceUri is missing (does it return empty connectionIds cleanly, or does it make 3 spurious lookups?).

Context for Agents
The fallback chain test (`tries metadata.googleCalendarWatchResourceUris first...`) asserts `toHaveBeenCalledTimes(4)` and hard-codes all four call orderings, but the mock is set up at the describe-suite level — specifically, `nangoMock.executeScriptForWebhooks = mock` replaces the method on the instance right before the call. However, each `it` block creates its own `nangoMock`, so the mock from the first test cannot leak into subsequent ones. That's fine. What IS missing is a test for the case where `x-goog-resource-uri` is absent from a google-calendar webhook: the function will still try all three email-based fallbacks with an undefined `connectionIdentifierValue`. Add a test asserting the behavior when `resourceUri` is missing (does it return empty connectionIds cleanly, or does it make 3 spurious lookups?).

File: packages/server/lib/webhook/google-calendar-webhook-routing.unit.test.ts
Line: 40

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants