Skip to content

Google Calendar Webhook

Route & Auth

Property Value
Method POST
Path /webhooks/google/calendar
Auth type secret
Header X-Goog-Channel-Token
Secret GOOGLE_WEBHOOK_TOKEN
Service account google-webhook@alan-eu-tools.iam.gserviceaccount.com
Response 204 No Content

Payload

Google Calendar push notifications carry data in headers, not the body:

Header Description
X-Goog-Channel-ID Watch channel identifier
X-Goog-Resource-ID Calendar resource identifier
X-Goog-Resource-State Event type: sync, exists, or not_exists
X-Goog-Resource-URI Resource URI
X-Goog-Message-Number Incrementing message number

Behavior

Only exists events trigger processing. The endpoint enqueues _process_refused_events as a background job.

The job:

  1. Finds the OncallGroup by calendar_watch_channel_id
  2. Fetches incremental calendar changes using the stored sync token
  3. Detects declined attendees on future shifts
  4. Marks declined shifts (declined_on = today)
  5. Attempts to find swap candidates via find_shift_for_swap()
  6. Swaps shifts and notifies via Slack

See the On-Call Groups > Shift declination and swap section for the full sequence diagram.

Integrations

  • Google Calendar API: incremental sync, event reading
  • Slack API: swap notifications
  • On-call group DB: shift management

Code reference

apps.eu_tools.webhooks.google_calendar.GoogleCalendarHeadersSchema

Bases: Schema

channel_expiraton class-attribute instance-attribute

channel_expiraton = String(
    data_key="X-Goog-Channel-Expiration"
)

channel_id class-attribute instance-attribute

channel_id = String(
    data_key="X-Goog-Channel-ID", required=True
)

channel_token class-attribute instance-attribute

channel_token = String(data_key='X-Goog-Channel-Token')

message_number class-attribute instance-attribute

message_number = Integer(
    data_key="X-Goog-Message-Number", required=True
)

resource_id class-attribute instance-attribute

resource_id = String(
    data_key="X-Goog-Resource-ID", required=True
)

resource_state class-attribute instance-attribute

resource_state = String(
    data_key="X-Goog-Resource-State",
    required=True,
    validate=OneOf(["sync", "exists", "not_exists"]),
)

resource_uri class-attribute instance-attribute

resource_uri = String(
    data_key="X-Goog-Resource-URI", required=True
)

apps.eu_tools.webhooks.google_calendar.GoogleCalendarWebhook

Bases: MethodView

post

post(headers)
Source code in apps/eu_tools/webhooks/google_calendar.py
@google_blueprint.arguments(schema=GoogleCalendarHeadersSchema, location="headers")
@google_blueprint.response(status_code=204)
def post(self, headers) -> None:  # type: ignore[no-untyped-def]
    if headers["resource_state"] == "exists":
        current_rq.get_queue(DEFAULT_PRIORITY).enqueue(
            _process_refused_events, headers["channel_id"]
        )

apps.eu_tools.webhooks.google_calendar.google_blueprint module-attribute

google_blueprint = CustomBlueprint(
    "google_webhook",
    "google_webhook",
    url_prefix="/webhooks/google",
    auth_context_providers=[
        WebhookAuthContextProvider(
            auth_type=secret,
            header_name="X-Goog-Channel-Token",
            secret_name_config_key="GOOGLE_WEBHOOK_TOKEN",
            auth_principal_type=ServiceAccount,
            auth_principal_email="google-webhook@alan-eu-tools.iam.gserviceaccount.com",
        )
    ],
)

OpenAPI

Google Calendar webhook on ReDoc ⧉