I recently helped create this post on “Migrating EWS notifications to Microsoft Graph,” where I tried to cover all the information you need to migrate from EWS to Graph Webhooks. For this article, I relied on some older projects I worked on and wrote some migration tests to ensure I had reasonable coverage for some of the issues you might encounter during the process.
I was originally going to publish that code in this post, but I decided instead to write something new that had a real-world purpose. This proved useful, as it brought a different dimension to some of the challenges you might face when building Graph Webhook apps against a mailbox.
Deleted Items Watcher
The app idea I had was to build a Deleted Items Watcher. This app would subscribe to all mail folders via the Messages endpoint, with separate subscriptions to RecoverableItems/Deletions and RecoverableItems/Purges. These latter two subscriptions were necessary to capture both permanently deleted items (e.g., using Shift-Delete in Outlook) and permanentDelete actions from Graph. If you are new to email development, you should first understand how Single Item Recovery and the Dumpster work in Exchange (see [link]).
The most important things my watcher needed to report were:
Which folder the items came from.
What type of delete was performed (which has to be derived from where the item originated).
The time, subject, and other details of the deleted message.
Architectural Challenges
As always, planning is one thing and implementing is another. While Graph webhooks can not tell you when a particular item is deleted in one folder and created in another (or in reality, it gets moved and pointers are updated in the ESE), unlike EWS notifications, you don’t get the “where” in terms of the folder it was moved from.
To cater to this, I used a couple of methods. The first was to build an item cache of all the updates that were happening . For example, if someone read a message and then deleted it, the Read would first register that the message was in the Inbox. Then, if they shift-deleted the message, it would trigger a create in the dumpster, and the itemcache would tell me that the last folder for this item was the Inbox. I also used the LAPFID (Last Active Parent Folder ID), which is a MAPI property that stores where an item was deleted from so that when you’re using Single Item Recovery, it can be restored to its original location. So that gave me two points of Last known location information which is useful for any auditing applications.
I also used a Folder cache to because I didn’t want to have to enumerate all the Mailbox folders and want it to be able to dynamically cater for people who might create folders or delete folders in a Mailbox.
I implemented Rich notifications and immutableId’s to minimise the number of request that where needed. In my case i could get all the information I needed from the rich notification. To keep with this throttling budget i used queuing for the webhooks so for one they events get processing synchronously and I used semaphoreSlim to ensure no more the 4 concurrent connection get used.
Another challenge I encountered was the folder deletion problem. Because webhooks don’t listen to folder-level events (eg update the folder name or location), deleting an entire folder full of items won't trigger any Item Webhook notifications. Unlike EWS, Microsoft Graph currently lacks notifications to tell you when a folder is added, updated, or moved. This is a significant gap in the Graph API that really needs improving; currently, the only way to catch these hierarchy changes is through inefficient pull synchronizations, which can be a real issue for certain use cases. I tried to work around this in my app by checking if the parent folder ID of the parent folder of an incoming item notification to see if it matched the DeleteItems folder or the Recoverable Items folders that where being monitored . While this provides some level of coverage, it isn’t a true solution to the problem.
I’ve put the code for the app up here
https://github.com/gscales/DeletedItemWatcher
Here some Sample output
┌─ Deleted Item Detected ───────────────────────────────
│ Deletion tier : DeletedItems
│ Current folder : \Deleted Items [System Folder: deleteditems]
│ Message ID : AQM...immutable-id...=
│ Detected at : 2026-06-30T02:12:19.000Z
│
│ Subject : Re: Party
│ From : Glen Scales <gscales@datarumble.com>
│ Received : 2026-06-30T01:55:00.000Z
│ Sent : 2026-06-30T01:54:55.000Z
│
│ ── Prior Location (from item cache) ──────────────
│ Was in folder : \Inbox
│ Folder type : System Folder: inbox
│ Cached at : 2026-06-30T01:55:02.000Z
│
│ ── LAPFID (Last Active Parent FolderId) ──────────
│ Last active folder : Inbox
│ Folder restId : AAMkAD...
│ LAPFID (base64) : AAAAALs3DlWu4qlHiwdYLMXzOS4...
│ LAPFID (hex) : 0000000...
│ (PR_LAST_ACTIVE_PARENT_ENTRYID / Binary 0x348A — the folder
│ this item was deleted from; restId above can be used as a /move target)
│
│ Cache size : 47 entries
└────────────────────────────────────────────────────────
┌─ Folder Deletion Detected (inferred) ─────────────────
│ Folder : Project X
│ Folder path : \Inbox\Project X
│ Folder ID : AAMkAD...
│ Moved into : DeletedItems
│ Detected via msg : AQM...immutable-id...=
│ Detected at : 2026-06-30T02:14:01.000Z
│ (Graph has no change-notification resource for mailFolder hierarchy
│ changes — this is inferred from a message's parentFolderId chain.)
└────────────────────────────────────────────────────────
