Guide · Integrations
Send Microsoft Bookings appointments to a CRM with Power Automate
Last updated 10 min read9 sources
This guide builds a Power Automate cloud flow. When someone books on a Microsoft Bookings shared booking page, the flow creates or updates a contact and an activity in your CRM. It checks for an existing record first so you don't get duplicates, copies across the answers to your custom questions and the campaign tracking value, and tells you when a run fails. It's written for Microsoft 365 admins and ops people who look after a booking page and a CRM (Dynamics 365/Dataverse, Salesforce or HubSpot). Allow about an hour for the first flow and 15 minutes for each flow after that.
What the connector gives you (as of September 2026)#
The Microsoft Bookings connector is the only event source Microsoft supports for Bookings. Microsoft Graph has no Bookings webhooks or change notifications. Before you design anything, know its limits:
| Item | Detail |
|---|---|
| Status | "Microsoft Bookings (Preview)". It is still in preview. |
| Licence class | Standard in Power Automate, Power Apps, Logic Apps and Copilot Studio |
| Triggers | "When a appointment is Created" (Microsoft's spelling), "When an appointment is Updated", "When an appointment is Cancelled" |
| Actions | One: "List Booking Businesses where user is an admin". It can't create, update or delete appointments. |
| Who can build | "Only Bookings admins can create flows using Appointment triggers." That means the Administrator role on the booking page, which is not the same as a tenant admin. |
| Flow cap | "Only 5 flows can be created per Bookings Mailbox". The cap is per page, not per person. A sixth flow returns HTTP 429. |
| Throttling | 100 API calls per connection every 60 seconds. Connections can't be shared. |
| Clouds | Not available in GCC, GCC High, DoD or 21Vianet |
| Personal pages | Not supported. The trigger needs a booking mailbox SMTP address, and Bookings with me pages don't have their own mailbox. |
Useful fields in the trigger payload:
SelfServiceAppointmentId: the appointment ID. Use it as your unique key. The olderIdfield is deprecated.CustomerName,CustomerEmail,CustomerPhone,CustomerNotes,CustomerTimeZone,CustomerLocation: filled for 1:1 bookings.Customers[]: the attendees of a group booking, each withName,Email,NotesandLocation.CustomQuestionAnswers[]: one item per question, withQuestion,Answer,AnswerOptionsandIsRequired.ServiceId,ServiceName,ServiceNotes,StartTime,EndTime,Duration.StaffMembers[]: each withDisplayName,EmailAddressandId.JoinWebURL: the Teams link, for online services.TrackingData: "Campaign tracking Data", theRefIDvalue from the booking link.CancelReason,FilledAttendeesCount,MaxAttendeesCount,IsSMSNotificationsEnabled.
Prerequisites#
- A Microsoft 365 licence that includes Bookings, for the account that will own the flow. The connector requires one.
- The Administrator role on the shared booking page. Team member, Scheduler, Viewer and Guest can't create appointment flows.
- A Power Automate licence that covers premium connectors, if your CRM connector is premium. Microsoft's connector reference lists Microsoft Dataverse, Salesforce and HubSpot CRM (Independent Publisher) as Premium in Power Automate. SharePoint, Excel Online (Business), Microsoft Teams and Office 365 Outlook are Standard. The designer labels premium connectors, so check your plan before you build.
- A CRM field where you can store the Bookings appointment ID, for example a custom text column called "Bookings appointment ID".
- SMTP enabled on the booking mailbox, and no Conditional Access or app-consent policy blocking the connector. In one Q&A thread, the only fix for a flow that never triggered was turning on SMTP for the mailbox.
Step 1: Get the booking page's SMTP address#
- Open Bookings and go to Shared booking pages, then choose the page.
- Open the page's Integrations area and select Power Automate. The page's SMTP address is shown there. Copy it.
The trigger's dropdown only lists pages you are an admin of and have opened recently. If your page isn't listed, choose Enter custom value and paste the address.
Step 2: Plan your five flows#
Each trigger needs its own flow, and the page allows five flows in total. A practical allocation:
- Created → CRM (this guide)
- Updated → CRM
- Cancelled → CRM
- One spare for notifications or Teams posts
- One spare kept for testing
If you need several destinations for the same event, add more actions to the existing flow. A second flow would use up your allowance.
Step 3: Build the Created flow and normalise the data#
Create an automated cloud flow and choose the trigger When a appointment is Created. Paste the SMTP address. Then add a Compose action for each value you need, so the CRM steps stay simple.
1:1 bookings fill the top-level customer fields. Group bookings fill Customers[] instead. This expression reads the email either way (the first attendee creates the appointment):
coalesce(triggerBody()?['CustomerEmail'], first(triggerBody()?['Customers'])?['Email'])To find the answer to one specific custom question, add Filter array:
From: @triggerBody()?['CustomQuestionAnswers']
Condition: @equals(item()?['Question'], 'Company name')Then read the answer with:
first(body('Filter_array'))?['Answer']Match on the question text exactly as it appears in Bookings. If someone renames the question, the filter stops matching. Agree with whoever edits the service that question text is part of the integration.
To put every answer into one CRM notes field, add Select with the map concat(item()?['Question'], ': ', item()?['Answer']) and join the results with a newline:
join(body('Select'), decodeUriComponent('%0A'))The staff member usually becomes the CRM owner:
first(triggerBody()?['StaffMembers'])?['EmailAddress']TrackingData holds the RefID from links like https://outlook.office.com/book/Sales@contoso.com/?RefID=linkedin_q4. Map it to a lead source or campaign field. It's the only attribution value Bookings records natively. For why it is sometimes blank, see RefID tracking not working.
Step 4: Write to your CRM#
Dynamics 365 Sales or Dataverse#
Use the Microsoft Dataverse connector. Microsoft marks the older "Dynamics 365" connector as deprecated.
- List rows on Contacts, with the filter
emailaddress1 eq 'alex@example.com', using the email Compose output in place of the address, and Row count set to 1. - Add a Condition: if the result is empty, Add a new row (Contact). If it isn't, Update a row using the contact ID you found.
- List rows on your activity table (for example Appointments), filtering on your custom "Bookings appointment ID" column. If nothing comes back, Add a new row with subject
ServiceName, start and end times, the owner, and a link to the contact. If a row exists, stop, because this booking has already been processed.
Salesforce#
Salesforce has an upsert action, which makes deduplication simpler.
- Create a custom text field marked as External ID (for example
Bookings_Appointment_Id__c) on the object you log meetings on. - Execute a SOQL query such as
SELECT Id FROM Contact WHERE Email = '...' LIMIT 1to find the contact. Create one if nothing comes back. - Insert or Update (Upsert) a Record by External ID (V2), using
SelfServiceAppointmentIdas the external ID value. Retries and duplicate triggers update the same record instead of creating a new one.
HubSpot#
The Power Automate catalogue lists HubSpot CRM (Independent Publisher) (Preview) as Premium. Its actions include Create a contact, Get a contact, Update a contact and List contacts. "Independent Publisher" means it is maintained by the community, not by HubSpot or Microsoft, so test it against your portal before you depend on it. The flow is the same shape: search by email, create or update the contact, then log the meeting with the appointment ID in a custom property. If the connector doesn't cover the object you need, some teams call HubSpot's API directly from Power Automate instead. Check whether that action is premium in your plan too.
Step 5: Deduplicate on SelfServiceAppointmentId#
Microsoft's connector FAQ says SelfServiceAppointmentId is filled for every kind of appointment (online, in person, staff-booked or customer-booked). The Id field is an Exchange ID that isn't unique across triggers. Always:
- Store
SelfServiceAppointmentIdon the CRM activity. - Look it up before you create an activity. With Salesforce, upsert on it instead.
- Match contacts on email, not name.
Step 6: Handle updates, group bookings and cancellations#
Updated flow. The payload doesn't say what changed, so overwrite start, end, staff and answers every time. Two cases need care:
- Group bookings. When a second or later person joins a group session, the connector fires Updated, not Created. Add an Apply to each over
Customersand run the same find-or-create contact logic for each attendee. UseFilledAttendeesCountif you track capacity. - Reliability. Users on Microsoft Q&A report the Updated trigger showing no runs for edits made in the Bookings calendar. Don't treat it as your only source of truth. Reconcile regularly against the Bookings export, which includes a Booking ID column, or against the Microsoft Graph Bookings API.
Cancelled flow. Find the activity by appointment ID and mark it cancelled. Don't rely on CancelReason: Microsoft lists it as not populated for 1:1 bookings.
Error handling#
- Put the CRM actions inside a Scope. After it, add a notification action (Teams or Outlook, both Standard) set to run only when the scope has failed or has timed out. Include the appointment ID and the run URL in the message.
- Keep the default retry policy on the CRM actions. Because the flow is keyed on the appointment ID, a retry updates the record rather than duplicating it.
- Stay under 100 calls per 60 seconds per connection. That's rarely a problem for one page, but a burst of group sign-ups with an Apply to each loop can reach it. Set the loop's concurrency to a low value.
- Know the documented errors: 403 (you aren't a Bookings admin on the page), 429 (the page already has five flows), 502 Bad Gateway (backend issue, report it to Microsoft 365 support), and 403 "Notification URL domain not a part of allowed list". Microsoft also notes that errors during flow creation aren't always shown in the portal.
- The flow runs on its owner's connection, and connections can't be shared. If the owner leaves or loses the Administrator role, the flow stops. Use an account your organisation will keep, with a Bookings licence.
Test it#
- Book a test appointment on the live page with a real inbox you control, answer every custom question, and add
?RefID=test_crmto the URL. - In the flow's run history, confirm one run for Created and check the Compose outputs.
- Check the CRM for one contact and one activity carrying the appointment ID and
test_crm. - Book the same slot again as a second attendee on a group service. Confirm the Updated flow adds that person.
- Cancel from the confirmation email. Confirm the Cancelled flow marks the activity.
If no run appears at all, work through why a Bookings trigger isn't firing.
Common problems#
| Symptom | Likely cause |
|---|---|
| Page missing from the trigger dropdown | You haven't opened it recently, or you aren't an admin on it. Paste the SMTP as a custom value. |
| No runs, no errors | SMTP disabled on the mailbox, or Conditional Access blocking the connection (invalid_grant) |
| HTTP 429 while saving | Five flows already exist for this page. Delete or combine flows. |
| Customer fields empty | Group service. Read Customers[] instead of the top-level fields. |
| Duplicate CRM activities | Keyed on Id or on name. Switch to SelfServiceAppointmentId. |
TrackingData blank | The link had no RefID, or the value used characters other than letters, numbers, underscore and hyphen |
For a broader look at CRM options beyond Power Automate, see sending Bookings appointments to HubSpot, Salesforce or Dynamics.
Doing this with BookingsXP#
If people book through a BookingsXP widget on your site, you can skip the connector's limits for those bookings. Opt in to the optional data layer on a saved widget, and BookingsXP sends a signed booking.created webhook (and booking.failed) to Zapier, Make, n8n, Power Automate or your own endpoint. It doesn't need a Bookings admin or count against the five-flow cap, and it carries the UTM, click ID and referrer the widget captured. It only sees bookings made through the widget. Bookings made on Microsoft's own page, by staff in the Bookings calendar, or cancelled from Microsoft's email links still need the connector. Webhooks are on paid plans; see /features/webhooks. BookingsXP is independent and not affiliated with or endorsed by Microsoft.
Questions people also ask
Sources
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn: Power automate integration (opens in a new tab) · learn.microsoft.com
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn (opens in a new tab) · learn.microsoft.com
- Microsoft Learn: Customize booking page (opens in a new tab) · learn.microsoft.com
- Microsoft Learn: Bookings when an appointment is updated (preview) (opens in a new tab) · learn.microsoft.com
- Microsoft Learn: Using bookings with power automate (opens in a new tab) · learn.microsoft.com