← Integrations
Calendar

Seshn + Google Calendar

Automatically sync bookings to Google Calendar — create events on booking, update on reschedule, remove on cancel.

Install

1npm install googleapis

Webhook events

Map booking lifecycle events to calendar operations:

booking.createdCreate a calendar event
booking.confirmedUpdate event status to confirmed
booking.rescheduledMove event to new time
booking.cancelledDelete the calendar event
booking.series_createdCreate a recurring calendar event

Setup

Use a Google Cloud service account for server-to-server calendar access. This avoids OAuth flows in your webhook handler.

  1. Create a project in the Google Cloud Console.
  2. Enable the Google Calendar API.
  3. Create a service account and download the JSON key.
  4. Share your staff calendar with the service account email.

Webhook handler

app/api/webhooks/seshn/route.ts
1import { google } from 'googleapis';
2import { createHmac } from 'crypto';
3
4const WEBHOOK_SECRET = process.env.SESHN_WEBHOOK_SECRET!;
5const CALENDAR_ID = process.env.GOOGLE_CALENDAR_ID!;
6
7const auth = new google.auth.GoogleAuth({
8 keyFile: process.env.GOOGLE_SERVICE_ACCOUNT_KEY_PATH,
9 scopes: ['https://www.googleapis.com/auth/calendar'],
10});
11
12const calendar = google.calendar({ version: 'v3', auth });
13
14export async function POST(req: Request) {
15 const body = await req.text();
16 const signature = req.headers.get('x-webhook-signature');
17
18 const expected = 'sha256=' +
19 createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex');
20
21 if (signature !== expected) {
22 return new Response('Invalid signature', { status: 401 });
23 }
24
25 const event = req.headers.get('x-webhook-event');
26 const payload = JSON.parse(body);
27
28 switch (event) {
29 case 'booking.created':
30 case 'booking.confirmed':
31 await calendar.events.insert({
32 calendarId: CALENDAR_ID,
33 requestBody: {
34 summary: `${payload.service.name} — ${payload.contact.name}`,
35 description: `Booking ${payload.id}\n${payload.seats} seat(s)`,
36 start: {
37 dateTime: payload.slot.startTime,
38 timeZone: payload.location.timezone,
39 },
40 end: {
41 dateTime: payload.slot.endTime,
42 timeZone: payload.location.timezone,
43 },
44 // Use booking ID as event ID for easy lookup later
45 id: payload.id.replace(/-/g, '').slice(0, 32),
46 },
47 });
48 break;
49
50 case 'booking.rescheduled':
51 const eventId = payload.id.replace(/-/g, '').slice(0, 32);
52 await calendar.events.patch({
53 calendarId: CALENDAR_ID,
54 eventId,
55 requestBody: {
56 start: {
57 dateTime: payload.newSlot.startTime,
58 timeZone: payload.location.timezone,
59 },
60 end: {
61 dateTime: payload.newSlot.endTime,
62 timeZone: payload.location.timezone,
63 },
64 },
65 });
66 break;
67
68 case 'booking.cancelled':
69 await calendar.events.delete({
70 calendarId: CALENDAR_ID,
71 eventId: payload.id.replace(/-/g, '').slice(0, 32),
72 }).catch(() => {
73 // Event may not exist — that's ok
74 });
75 break;
76 }
77
78 return new Response('OK', { status: 200 });
79}

Register the webhook

terminal
1curl -X POST https://api.seshn.net/v1/webhooks \
2 -H "Authorization: Bearer sk_live_..." \
3 -H "Content-Type: application/json" \
4 -d '{
5 "url": "https://yourdomain.com/api/webhooks/seshn",
6 "events": [
7 "booking.created",
8 "booking.confirmed",
9 "booking.rescheduled",
10 "booking.cancelled",
11 "booking.series_created"
12 ]
13 }'

Next steps

  • Add attendees to the event to send Google Calendar invites to customers automatically.
  • For booking.series_created, use Google Calendar's recurrence rules (RRULE) instead of individual events.
  • Store the Google Calendar event ID in Seshn booking metadata for two-way sync.