Hey all,
Just dropping my use case here for anyone that may need to do the same thing in future.
Client required all events to be saved to MS Calender and make a teams event for them, as-well as update /cancel when bookings are updated.
Uses MS Graph so you will need to create an App registration and give it these perms
Calendars.ReadWrite & OnlineMeetings.ReadWrite
Env file
GRAPH_CLIENT_ID="<UUID>"
# Client secret value not the secret ID
GRAPH_CLIENT_SECRET="Secret"
GRAPH_TENANT_ID="<UUID>"
GRAPH_CALENDAR_USER_ID="<Client email>"
Setup a flow for create| cancel | and reschedule and point to your function
In action it is “Create”|“Cancel”|“Reschedule”
--Headers--
{
"Content-Type": "application/json"
}
--Body--
{
"Action": "Cancel", //"Create"|"Cancel"|"Reschedule"
"BookingId":"{{AppointmentID}}",
"OrderId":"{{OrderID}}",
"CustomerName": "{{CustomerName}}",
"CustomerEmail": "{{CustomerEmail}}",
"DateTime": "{{DateTime}}",
"ServiceName": "{{ServiceName}}",
"ServiceDuration": "{{ServiceDuration}}",
"CustomerManagementLink": "{{CustomerManagementLink}}",
"VariantTitle": "{{VariantTitle}}"
}
Here is the netlify function (Can be in whatever api / function handler you want.
// Netlify function to create, cancel, or reschedule a Microsoft Teams meeting via Microsoft Graph API
import type { Handler } from "@netlify/functions";
import { Client } from "@microsoft/microsoft-graph-client";
import { ClientSecretCredential } from "@azure/identity";
// Load required environment variables for Microsoft Graph authentication
const {
GRAPH_CLIENT_ID,
GRAPH_CLIENT_SECRET,
GRAPH_TENANT_ID,
GRAPH_CALENDAR_USER_ID,
} = process.env;
// Main Netlify handler function
export const handler: Handler = async (event) => {
// Check for required credentials
if (
!GRAPH_CLIENT_ID ||
!GRAPH_CLIENT_SECRET ||
!GRAPH_TENANT_ID ||
!GRAPH_CALENDAR_USER_ID
) {
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ error: "Missing Microsoft Graph credentials" }),
};
}
try {
// Parse incoming request body
const data = JSON.parse(event.body || "{}");
// Authenticate with Microsoft Graph using client credentials
const credential = new ClientSecretCredential(
GRAPH_TENANT_ID,
GRAPH_CLIENT_ID,
GRAPH_CLIENT_SECRET
);
// Initialize Microsoft Graph client
const client = Client.initWithMiddleware({
authProvider: {
getAccessToken: async () =>
(await credential.getToken("https://graph.microsoft.com/.default"))!
.token,
},
});
// Determine if the appointment is virtual
const isVirtual = data.VariantTitle?.includes("Virtual");
// Determine action type (create, cancel, reschedule)
const action: "create" | "cancel" | "reschedule" =
data.Action?.toLowerCase() || "create";
const orderId = data.OrderId || null;
const bookingId = data.BookingId || null;
// Build meeting subject line
// NOTE: Replace <Client Name> with your actual client or business name, or dynamically inject as needed
const subject = `<Client Name> - ${
isVirtual ? "[Virtual]" : ""
} Appointment: ${data.ServiceName}`;
// Handle cancellation
if (action === "cancel") {
// Attempt to cancel event by order/booking metadata
const { success, error, value, code } = await cancelEventByMeta(
client,
orderId,
bookingId
);
return {
statusCode: code,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
error,
success,
value,
}),
};
}
// Handle rescheduling
if (action === "reschedule") {
// Attempt to reschedule event by order/booking metadata
const { success, error, value, code } = await rescheduleEventByMeta(
client,
orderId,
bookingId,
data.DateTime,
data.ServiceDuration || 25
);
return {
statusCode: code,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
error,
success,
value,
}),
};
}
// Handle creation
if (action === "create") {
// Attempt to create event by order/booking metadata
const { success, error, value, code } = await createEventByMeta(
client,
orderId,
bookingId,
data.DateTime,
data.ServiceDuration || 25,
isVirtual,
subject,
data.CustomerName,
data.CustomerEmail,
data.VariantTitle,
data.ManageLink || ""
);
return {
statusCode: code,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
error,
success,
value,
}),
};
}
} catch (err: any) {
// Catch-all error handler
console.error("Microsoft Graph error:", err);
return {
statusCode: 500,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
error: "Failed to create Microsoft event",
err,
body: JSON.parse(event.body || "{}"),
}),
};
}
};
// Cancel a Teams event by searching for order/booking metadata
async function cancelEventByMeta(
client: Client,
orderId: string | null,
bookingId: string | null
): Promise<{
success: boolean;
value?: any;
error?: string;
code?: number;
}> {
try {
// Find event by metadata
const event = await findEventByMeta(client, orderId, bookingId);
if (event && event.success) {
// Delete the event if found
await client
.api(`/users/${GRAPH_CALENDAR_USER_ID}/events/${event.value.id}`)
.delete();
return {
code: 200,
success: true,
value: {
message: "Meeting cancelled",
eventId: event.value.id,
},
};
} else {
return {
code: 404,
success: false,
error: "Meeting not found to cancel",
};
}
} catch (err) {
return {
code: 500,
success: false,
error: "Failed to cancel meeting - " + err.message,
};
}
}
// Reschedule a Teams event by searching for order/booking metadata
async function rescheduleEventByMeta(
client: Client,
orderId: string | null,
bookingId: string | null,
newDateTime: string,
newDuration: number
): Promise<{
success: boolean;
value?: any;
error?: string;
code?: number;
}> {
try {
// Find event by metadata
const event = await findEventByMeta(client, orderId, bookingId);
if (event && event.success) {
// Calculate new start and end times
const startTime = new Date(newDateTime).toISOString();
const endTime = new Date(
new Date(newDateTime).getTime() + newDuration * 60000
).toISOString();
// Update the event with new times
await client
.api(`/users/${GRAPH_CALENDAR_USER_ID}/events/${event.value.id}`)
.update({
start: { dateTime: startTime, timeZone: "Australia/Sydney" },
end: { dateTime: endTime, timeZone: "Australia/Sydney" },
});
return {
code: 200,
success: true,
value: {
message: "Meeting rescheduled",
eventId: event.value.id,
},
};
} else {
return {
code: 404,
success: false,
error: "Meeting not found to reschedule",
};
}
} catch (err) {
return {
code: 500,
success: false,
error: "Failed to reschedule meeting - " + err.message,
};
}
}
// Create a Teams event, or return existing if duplicate found
async function createEventByMeta(
client: Client,
orderId: string | null,
bookingId: string | null,
dateTime: string,
duration: number,
isVirtual: boolean,
subject: string,
customerName: string,
customerEmail: string,
variantTitle: string,
manageLink: string
): Promise<{
success: boolean;
value?: any;
error?: string;
code?: number;
}> {
try {
// Check for duplicate event
const event = await findEventByMeta(client, orderId, bookingId);
//Event already exists
if (event && event.success) {
return {
code: 200,
success: true,
value: {
message: "Duplicate appointment detected",
meetLink: event?.value?.onlineMeeting?.joinUrl || null,
existingEventId: event?.value?.id,
},
};
}
// Calculate start and end times
const startTime = new Date(dateTime).toISOString();
const endTime = new Date(
new Date(dateTime).getTime() + duration * 60000
).toISOString();
// Build HTML body for meeting invite
const bodyHtml = makeBody({
name: customerName,
email: customerEmail,
variantTitle: variantTitle,
manageLink: manageLink,
isVirtual: isVirtual,
appointmentDateTime: dateTime,
appointmentDuration: duration,
orderId: orderId,
bookingId: bookingId,
});
// Build event object for Graph API
const eventBody: any = {
subject: subject,
body: {
contentType: "HTML",
content: bodyHtml,
},
start: {
dateTime: startTime,
timeZone: "Australia/Sydney",
},
categories: [
"sesami-booking-auto",
orderId ? `order-${orderId}` : undefined,
bookingId ? `booking-${bookingId}` : undefined,
].filter(Boolean),
end: {
dateTime: endTime,
timeZone: "Australia/Sydney",
},
attendees: [
{
emailAddress: {
address: customerEmail,
name: customerName,
},
type: "required",
},
],
};
// If virtual, enable Teams meeting
if (isVirtual) {
eventBody.isOnlineMeeting = true;
eventBody.onlineMeetingProvider = "teamsForBusiness";
}
// Create the event via Graph API
const response = await client
.api(`/users/${GRAPH_CALENDAR_USER_ID}/events`)
.post(eventBody);
return {
code: 200,
success: true,
value: {
message: "Meeting created successfully",
meetLink: response?.onlineMeeting?.joinUrl || null,
eventId: response.id,
isVirtual,
sentTo: customerEmail,
sentFrom: GRAPH_CALENDAR_USER_ID,
},
};
} catch (err) {
return {
code: 500,
success: false,
error: "Failed to create meeting - " + err.message,
};
}
}
// Find a Teams event by order/booking metadata (categories)
async function findEventByMeta(
client: Client,
orderId: string,
bookingId: string
) {
try {
let filter = [];
if (orderId) filter.push(`categories/any(c:c eq 'order-${orderId}')`);
if (bookingId) filter.push(`categories/any(c:c eq 'booking-${bookingId}')`);
const filterStr = filter.join(" or ");
// Query Graph API for events with matching categories
const response = await client
.api(`/users/${GRAPH_CALENDAR_USER_ID}/events`)
.filter(filterStr)
.top(1)
.get();
const found = response.value?.[0] || null;
return {
success: !!found,
value: found,
};
} catch (err) {
console.error("Error finding event by metadata:", err);
return {
success: false,
error: err.message,
};
}
}
// Helper to generate HTML body for meeting invite
function makeBody({
name,
email,
variantTitle,
manageLink,
isVirtual,
appointmentDateTime,
appointmentDuration,
orderId,
bookingId,
}: {
name: string;
email: string;
variantTitle: string;
manageLink: string;
isVirtual: boolean;
appointmentDateTime: string;
appointmentDuration: number;
orderId?: string | null;
bookingId?: string | null;
}) {
// NOTE: The following template contains placeholder values such as <Client address>, <Client phone>, and <Client name>.
// Make sure to replace these with actual values or inject them dynamically from a secure source.
return `Hey ${name},<br/>
<br/>
Thanks for booking with me! The booking details are below:<br/>
<br/>
đź—“ Date & Time: ${appointmentDateTime}<br/>
⏳ Duration: ${appointmentDuration} minutes<br/>
đź“’ Session Type: ${variantTitle}<br/>
Order Id:${orderId}<br/>
Booking Id:${bookingId}<br/>
${!isVirtual ? `📍 Location: <Client address> ><br/>` : ""}<br/>
${
isVirtual
? `All Virtual sessions are held via Teams. To join, simply click the “Join the meeting now” button below”. When the session time commences, I will admit you into the call.<br/>
If you are having issues joining the call, feel free to call me on <a href="tel:<Client phone>"><Client phone>></a>.<br/>
`
: ""
}
<br/>
Regards,<br/>
<Client name>><br/>
`;
}