Skip to main content
Technology & EngineeringScheduling Services306 lines

Microsoft Bookings

Microsoft Bookings integration via Microsoft Graph API for enterprise appointment scheduling and calendar management

Quick Summary16 lines
You are an expert in integrating Microsoft Bookings via the Microsoft Graph API for enterprise scheduling and calendar management.

## Key Points

- Always include `timeZone` with datetime values; Graph API datetime objects without timezone context default to UTC, which causes scheduling errors for end users.
- Use delegated permissions (user-context tokens) for user-facing apps and application permissions (client credentials) for background services and daemons.
- Leverage the native Teams integration by setting `isLocationOnline: true` — this auto-generates a Teams meeting link for each appointment.
- Use `$select` query parameters to reduce payload size when listing appointments or services in high-volume scenarios.
- Map staff members to Azure AD users so that appointments automatically appear on their Outlook calendars.
- The Bookings API requires a Microsoft 365 Business Premium or higher license; Business Basic/Standard plans do not include API access.
- `calendarView` requires both `start` and `end` query parameters and returns a flat list, not paginated by default — add `$top` for large date ranges.
- Deleting a service that has future appointments will fail; cancel or reassign existing appointments first.
- Token expiration is 60 minutes for Graph API tokens; implement token caching with refresh logic rather than acquiring a new token per request.
- The `defaultDuration` field uses ISO 8601 duration format (`PT30M`, `PT1H`), not minutes as an integer — passing a number will cause a 400 error.
skilldb get scheduling-services-skills/Microsoft BookingsFull skill: 306 lines
Paste into your CLAUDE.md or agent config

Microsoft Bookings — Scheduling Integration

You are an expert in integrating Microsoft Bookings via the Microsoft Graph API for enterprise scheduling and calendar management.

Core Philosophy

Overview

Microsoft Bookings is an enterprise appointment scheduling service included in Microsoft 365 Business and Enterprise plans. It integrates natively with Outlook calendars, Teams meetings, and Azure AD. The Microsoft Graph API provides programmatic access to booking businesses, services, staff, customers, and appointments. Bookings is ideal when your organization already uses the Microsoft 365 ecosystem and needs scheduling that ties into existing identity, calendar, and video conferencing infrastructure.

Setup & Configuration

App Registration in Azure AD

# 1. Register an app at https://portal.azure.com > App registrations
# 2. Add the following API permissions:
#    - Bookings.Read.All
#    - Bookings.ReadWrite.All
#    - Bookings.Manage.All (for creating booking businesses)
#    - BookingsAppointment.ReadWrite.All

# 3. Set redirect URI and generate a client secret

Authentication with MSAL

import {
  ConfidentialClientApplication,
  Configuration,
} from "@azure/msal-node";

const msalConfig: Configuration = {
  auth: {
    clientId: process.env.AZURE_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
    clientSecret: process.env.AZURE_CLIENT_SECRET!,
  },
};

const msalClient = new ConfidentialClientApplication(msalConfig);

async function getAccessToken(): Promise<string> {
  const result = await msalClient.acquireTokenByClientCredential({
    scopes: ["https://graph.microsoft.com/.default"],
  });

  if (!result?.accessToken) {
    throw new Error("Failed to acquire access token");
  }
  return result.accessToken;
}

Graph Client Setup

import axios, { AxiosInstance } from "axios";

async function createGraphClient(): Promise<AxiosInstance> {
  const token = await getAccessToken();

  return axios.create({
    baseURL: "https://graph.microsoft.com/v1.0",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
  });
}

Core Patterns

Managing Booking Businesses

interface BookingBusiness {
  id: string;
  displayName: string;
  email: string;
  phone: string;
  businessType: string;
  address: {
    street: string;
    city: string;
    state: string;
    countryOrRegion: string;
    postalCode: string;
  };
  schedulingPolicy: {
    timeSlotInterval: string; // ISO 8601 duration, e.g. "PT30M"
    minimumLeadTime: string;
    maximumAdvance: string;
    allowStaffSelection: boolean;
  };
}

async function listBookingBusinesses(): Promise<BookingBusiness[]> {
  const client = await createGraphClient();
  const { data } = await client.get("/solutions/bookingBusinesses");
  return data.value;
}

async function getBookingBusiness(
  businessId: string
): Promise<BookingBusiness> {
  const client = await createGraphClient();
  const { data } = await client.get(
    `/solutions/bookingBusinesses/${businessId}`
  );
  return data;
}

Defining Services

interface BookingService {
  id: string;
  displayName: string;
  description: string;
  defaultDuration: string; // ISO 8601 duration
  defaultPrice: number;
  defaultPriceType: "undefined" | "fixedPrice" | "free";
  isLocationOnline: boolean;
  staffMemberIds: string[];
  schedulingPolicy?: {
    allowStaffSelection: boolean;
  };
}

async function createService(
  businessId: string,
  service: Partial<BookingService>
): Promise<BookingService> {
  const client = await createGraphClient();
  const { data } = await client.post(
    `/solutions/bookingBusinesses/${businessId}/services`,
    {
      displayName: service.displayName,
      description: service.description,
      defaultDuration: service.defaultDuration || "PT60M",
      defaultPrice: service.defaultPrice || 0,
      defaultPriceType: service.defaultPriceType || "free",
      isLocationOnline: service.isLocationOnline ?? true,
    }
  );
  return data;
}

Creating Appointments

interface BookingAppointment {
  id: string;
  serviceId: string;
  serviceName: string;
  startDateTime: {
    dateTime: string;
    timeZone: string;
  };
  endDateTime: {
    dateTime: string;
    timeZone: string;
  };
  customers: {
    emailAddress: string;
    name: string;
    phone?: string;
  }[];
  staffMemberIds: string[];
  isLocationOnline: boolean;
  onlineMeetingUrl?: string;
}

async function createAppointment(
  businessId: string,
  serviceId: string,
  customer: { name: string; email: string },
  startDateTime: string,
  timeZone: string
): Promise<BookingAppointment> {
  const client = await createGraphClient();
  const { data } = await client.post(
    `/solutions/bookingBusinesses/${businessId}/appointments`,
    {
      serviceId,
      startDateTime: { dateTime: startDateTime, timeZone },
      customers: [
        {
          emailAddress: customer.email,
          name: customer.name,
        },
      ],
      isLocationOnline: true,
    }
  );
  return data;
}

Querying Availability (via Calendar View)

async function getCalendarView(
  businessId: string,
  startDate: string,
  endDate: string
): Promise<BookingAppointment[]> {
  const client = await createGraphClient();
  const { data } = await client.get(
    `/solutions/bookingBusinesses/${businessId}/calendarView`,
    {
      params: {
        start: startDate,
        end: endDate,
      },
    }
  );
  return data.value;
}

// Get staff availability by checking existing appointments
// and comparing against business hours
async function getStaffAvailability(
  businessId: string,
  staffId: string,
  date: string
): Promise<{ start: string; end: string }[]> {
  const client = await createGraphClient();
  const { data } = await client.get(
    `/solutions/bookingBusinesses/${businessId}/staffMembers/${staffId}`
  );

  const workingHours = data.workingHours;
  const existingAppointments = await getCalendarView(
    businessId,
    `${date}T00:00:00`,
    `${date}T23:59:59`
  );

  // Filter to this staff member's appointments and compute open slots
  const staffAppointments = existingAppointments.filter((appt) =>
    appt.staffMemberIds.includes(staffId)
  );

  // Return free slots (implementation depends on your slot logic)
  return computeFreeSlots(workingHours, staffAppointments, date);
}

Canceling Appointments

async function cancelAppointment(
  businessId: string,
  appointmentId: string,
  cancellationMessage?: string
): Promise<void> {
  const client = await createGraphClient();
  await client.post(
    `/solutions/bookingBusinesses/${businessId}/appointments/${appointmentId}/cancel`,
    {
      cancellationMessage:
        cancellationMessage || "This appointment has been canceled.",
    }
  );
}

Best Practices

  • Always include timeZone with datetime values; Graph API datetime objects without timezone context default to UTC, which causes scheduling errors for end users.
  • Use delegated permissions (user-context tokens) for user-facing apps and application permissions (client credentials) for background services and daemons.
  • Leverage the native Teams integration by setting isLocationOnline: true — this auto-generates a Teams meeting link for each appointment.
  • Use $select query parameters to reduce payload size when listing appointments or services in high-volume scenarios.
  • Map staff members to Azure AD users so that appointments automatically appear on their Outlook calendars.

Common Pitfalls

  • The Bookings API requires a Microsoft 365 Business Premium or higher license; Business Basic/Standard plans do not include API access.
  • calendarView requires both start and end query parameters and returns a flat list, not paginated by default — add $top for large date ranges.
  • Deleting a service that has future appointments will fail; cancel or reassign existing appointments first.
  • Token expiration is 60 minutes for Graph API tokens; implement token caching with refresh logic rather than acquiring a new token per request.
  • The defaultDuration field uses ISO 8601 duration format (PT30M, PT1H), not minutes as an integer — passing a number will cause a 400 error.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add scheduling-services-skills

Get CLI access →