Outlook integration: methods and functional background
The Outlook integration synchronises appointments between a Microsoft 365 / Outlook calendar and i-Reserve. It talks directly to the Microsoft Graph REST API v1.0 over OAuth 2.0 — no EWS (Exchange Web Services) and no MSAL SDK are used. This article describes the functional background: which connection methods exist, how synchronisation works and what the limitations are.
Two connection methods
The method is selected with the authentication mode (config key auth_mode). Both methods can be served by the same Azure app; they are separate permission types in Entra ID.
| Delegated | Application (app-only) | |
|---|---|---|
| When | One personal or shared calendar | Many room / resource mailboxes, at scale |
| Authentication | Interactive OAuth (authorization code). A single user signs in and consents; i-Reserve stores an access and refresh token. | Non-interactive (client credentials). The app authenticates as itself and fetches an app-only token (~1h TTL, no refresh token). |
| Calendar source | The connected user's default calendar (me/…). | Per room mailbox (users/{room-upn}/…), each mapped to its own i-Reserve object. |
| Direction | Bidirectional: inbound (Outlook → i-Reserve) and outbound (i-Reserve → Outlook). | Inbound only. Outbound is disabled because an app-only token has no signed-in user (me). |
| Object mapping | One i-Reserve object. | One object per room (room → object table). Scales to hundreds of rooms per customer. |
Rule of thumb: use delegated for one calendar and one object. Use application as soon as you want to connect room / resource mailboxes — those cannot sign in interactively and have no licence, so delegated does not work for them.
How synchronisation works
Changes arrive near-real-time through a Graph change-notification subscription (webhook); a cron process catches missed notifications via polling. The integration deliberately uses both channels.
- Inbound reads with a Graph delta query (
calendarView/delta) over a −90 to +365 day window, pages through@odata.nextLinkand persists the@odata.deltaLinkas the sync token. In application mode each room has its own sync token and its own webhook. - Webhooks post to a public endpoint. A subscription expires after 3 days and is auto-renewed by cron once it is within 2 days of expiry. In application mode there is one subscription per room; notifications are routed by subscription id so exactly the right room is synced.
- Polling fallback: if a calendar had no sync for more than 30 minutes, cron queues an inbound sync so missed notifications are recovered.
Inbound: from Outlook appointment to booking
For each Outlook event i-Reserve decides whether it is a create, update or cancellation:
- Match an existing booking — first on the stored external event id (field
ires_field), falling back to a[booking_id]suffix in the subject. - Removed / cancelled (
@removedorisCancelled) → the linked booking gets the cancel status and is unlinked (unless it is in a lock status). - Existing booking → date and time are updated, gated by “allow booking update” and “ignore validation errors”. Bookings created through this integration always follow the Outlook change.
- New → a booking is created on the mapped object (channel
external/outlook), with subject and description as a remark. If the organiser matches a known customer, that customer is linked and (if configured) the “status with customer” is used.
Outbound (delegated only)
In delegated mode i-Reserve pushes appointments to Outlook based on booking status: create (POST me/events), update (PATCH me/events/{id}) and delete (DELETE me/events/{id}), driven by the “create calendar entry” and “delete on status” statuses. Optionally a Teams meeting link is created and the customer added as an attendee.
Limitations
- Outbound is delegated-only. In application mode outbound triggers are deliberately skipped and logged.
- Per-room state (sync token, webhook) is stored in the integration config; this scales to hundreds of rooms, but one integration = one Azure app/tenant.
- An app-only token lives ~1h and is refreshed automatically; there is no refresh token.
- There is no Graph room-list (
places) auto-discovery: room UPNs are entered manually.





