A project office I work with had an old SharePoint Server rule: when someone adds a project, SharePoint gives it an ID and builds a folder set in the documents library. Now they’re on SharePoint Online and asked me to “just move the event receiver over.”
That’s where most guides go wrong. Server-side code can’t run in SharePoint Online, and the cloud’s remote event receivers are being retired. Today the answer is a webhook or a flow.
In this tutorial, we’ll see what a sharepoint event receiver is, build the classic on-premises one, check the remote event receiver retirement dates, then cover the modern path: webhooks with GetChanges on a real list, plus Power Automate.
Quick answer: On SharePoint Server, inherit SPItemEventReceiver, override ItemAdding or ItemAdded, and deploy a farm solution. In SharePoint Online, don’t build remote event receivers: ACS-based ones stopped on April 2, 2026, and Entra-registered ones stop on July 1, 2027. Use a webhook (POST /_api/web/lists('<list id>')/subscriptions) plus GetChanges, or a Power Automate trigger.
What Is a SharePoint Event Receiver?
An event receiver is code that SharePoint runs when a site, list or item changes. It’s the reverse of the calls in my SharePoint REST API tutorial: SharePoint calls your code instead of you calling SharePoint.
Synchronous receivers end in -ing (ItemAdding, ItemDeleting) and run before the save, so they can cancel it. Asynchronous ones end in -ed (ItemAdded, ItemUpdated) and run after it. The main base classes are SPItemEventReceiver (items and documents), SPListEventReceiver (lists and columns) and SPWebEventReceiver (sites).
Classic Event Receiver Example (SharePoint Server Only)
This version runs only on SharePoint Server, because it ships as a farm solution (.wsp). In Visual Studio, add a new item and pick Event Receiver:

Choose List Item Events, the event source, and An item was added:

Visual Studio adds the class and an Elements.xml that binds it to the list. Here’s the receiver:
using Microsoft.SharePoint;
namespace SPGuides.ProjectReceivers
{
public class ProjectFolders : SPItemEventReceiver
{
public override void ItemAdded(SPItemEventProperties properties)
{
SPListItem project = properties.ListItem;
string projectId = "PRJ-" + project.ID.ToString("D4");
EventFiringEnabled = false;
project["ProjectID"] = projectId;
project.SystemUpdate(false);
EventFiringEnabled = true;
SPFolder root = properties.Web.Lists["Project Documents"].RootFolder.SubFolders.Add(projectId);
foreach (string name in new[] { "Analysis", "Design", "Development", "Testing", "Deployment" })
{
root.SubFolders.Add(name);
}
}
}
}
It stamps an ID such as PRJ-0004 and creates the folder set. Turning off EventFiringEnabled around SystemUpdate stops your own update from firing ItemUpdated again.

In ItemUpdating, new values are in AfterProperties. On a list, BeforeProperties is empty, so read old values from properties.ListItem; on a library, BeforeProperties holds them.
Remote Event Receivers in SharePoint Online: Retirement Dates
Remote event receivers (RERs) were the cloud version: a WCF service in a provider-hosted add-in, called over SOAP and authenticated through Azure ACS, which Microsoft switched off. Many old SharePoint Designer workflows died the same way.
| What | Date | Status |
|---|---|---|
| SharePoint Add-in model deprecated | November 27, 2023 | No new investment |
| ACS-registered remote event receivers | April 2, 2026 | Stopped working |
| SharePoint Add-ins in SharePoint Online | April 2, 2026 | Retired |
| Entra-registered remote event receivers | July 1, 2027 | Last day; all RERs stop |
| Add-ins on SharePoint Server | No date | Still supported on-premises |
So if you’re searching for how to create a remote event receiver in SharePoint Online, don’t. Microsoft points to webhooks or Microsoft Graph change notifications instead.
SharePoint Webhooks: The Modern Replacement
A webhook is a subscription on a list or library. When items change, SharePoint sends an HTTP POST to your URL:
- You subscribe with the list, your notification URL and an expiration date up to 180 days out.
- SharePoint calls your URL with a
validationtoken; you must echo it back, or no subscription. - On changes, SharePoint POSTs a small (possibly batched) notification, and you call
GetChanges. - You renew before the expiration date.
Webhooks cover 11 list events and are asynchronous only, so there’s no ItemAdding and no way to cancel a change.
Check which subscriptions a list already has
Before adding anything, I list what’s there (read-only, once you install PnP PowerShell):
$list = Get-PnPList -Identity "SPGDemo Orders"
"List ID: $($list.Id)"
(Invoke-PnPSPRestMethod -Url "/_api/web/lists('$($list.Id)')/subscriptions").value |
Format-List id, clientState, expirationDateTime, resource, notificationUrl

I hadn’t created any, yet the list returned one: clientState starts with system-managed and the URL is Microsoft’s pushnp.svc.ms service. Leave those alone. On a list with flows, I also found subscriptions to azure-apihub.net/apim/sharepointonline, the Power Automate connector’s own webhooks.
What a subscription looks like inside SharePoint
Then I listed the event receivers on the same list:
Get-PnPEventReceiver -List "SPGDemo Orders" |
Format-Table EventType, Synchronization, ReceiverClass -AutoSize

There are 11 rows, all asynchronous, all of class SPWebhookItemEventReceiver. A webhook is still an event receiver under the hood, one per supported event, owned by SharePoint instead of your code.
Create a webhook subscription
This call only works once you have your own public HTTPS endpoint that answers the validation handshake (next section). I didn’t run it on the demo site; the syntax is checked:
# Replace the URL with YOUR public HTTPS endpoint first
$list = Get-PnPList -Identity "SPGDemo Orders"
$body = @{
resource = "https://spguides.sharepoint.com/sites/SPGuides/_api/web/lists('$($list.Id)')"
notificationUrl = "https://<your-function-app>.azurewebsites.net/api/SpgWebhook"
expirationDateTime = (Get-Date).ToUniversalTime().AddDays(179).ToString("o")
clientState = "spg-orders-7f3a"
}
Invoke-PnPSPRestMethod -Method Post -Url "/_api/web/lists('$($list.Id)')/subscriptions" -Content $body
# Renew before day 180 (Add-PnPWebhookSubscription is the cmdlet for the create)
Set-PnPWebhookSubscription -List "SPGDemo Orders" -Subscription "<subscription id>" -ExpirationDate (Get-Date).AddDays(179)
It returns 201 Created with the subscription id. Check clientState on every notification so you can reject calls that aren’t yours.
The endpoint: validation handshake and notification
An HTTP-triggered Azure Function is the simplest host (the same kind you’d call from an SPFx web part). This handler compiles against ASP.NET Core and does both jobs:
public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
// 1. Validation handshake: echo the token as plain text
string validationToken = req.Query["validationtoken"];
if (!string.IsNullOrEmpty(validationToken))
{
return new ContentResult { Content = validationToken, ContentType = "text/plain", StatusCode = 200 };
}
// 2. Notification: queue it, answer fast
string body = await new StreamReader(req.Body).ReadToEndAsync();
log.LogInformation("SharePoint notification: {Body}", body);
return new OkResult();
}
Answer within 5 seconds; failed calls are retried 5 times, 5 minutes apart, then dropped. A notification (no item ID, no fields) looks like this:
{ "value": [ {
"subscriptionId": "<subscription id>", "clientState": "spg-orders-7f3a",
"expirationDateTime": "2026-12-31T16:00:00.0000000Z",
"resource": "01cea0a6-d7b5-419f-a0cc-99c3e10df216", "tenantId": "<tenant id>",
"siteUrl": "/sites/SPGuides", "webId": "bfb26070-6ae5-40ba-8756-a7725212c22e"
} ] }
Read what changed with GetChanges
When a notification arrives, ask the list for changes since your last token. I ran this read-only on SPGDemo Orders:
$url = "/_api/web/lists/getbytitle('SPGDemo Orders')/GetChanges"
$q = @{ Add = $true; Update = $true; DeleteObject = $true; Item = $true; FetchLimit = "3" }
$first = (Invoke-PnPSPRestMethod -Method Post -Url $url -Content @{ query = $q }).value
$first | Format-Table ItemId, ChangeType, Time -AutoSize
# Only changes after the last token
$token = $first[-1].ChangeToken.StringValue
$q.ChangeTokenStart = @{ StringValue = $token }
(Invoke-PnPSPRestMethod -Method Post -Url $url -Content @{ query = $q }).value |
Format-Table ItemId, ChangeType, Time -AutoSize

The first call returned items 1, 2 and 3 with ChangeType 1 (Add). With the last token as ChangeTokenStart, the second call returned only items 4, 5 and 6. Store that token after each run.
Power Automate: The No-Code Event Receiver
For business rules like the project folders, I’d use Power Automate first. The trigger When an item is created or modified covers ItemAdded and ItemUpdated, and trigger conditions keep it from running on every edit. Flows can’t block a save either.
For a worked example, see how to send a custom email when a new item is created. More list automation ideas are in the SharePoint tutorials section.
Which One Should You Use?
| Need | Use |
|---|---|
| Cancel or validate a change before it saves (on-premises) | Classic synchronous receiver (ItemAdding, ItemUpdating) |
| React to changes in SharePoint Online, no code | Power Automate trigger |
| React in your own app, at scale | SharePoint webhook + GetChanges |
Common Errors and How to Fix Them
- Subscription isn’t created. Your endpoint didn’t echo the
validationtokenas plain text with 200 in time. Test your URL with?validationtoken=abcfirst. - Notifications stop after a few months. The subscription expired (180 days max). Schedule renewals.
- GetChanges returns 400 “Cannot convert a primitive value to the expected type ‘Edm.Int64′”. I got this sending
FetchLimitas a number from PowerShell. Send it as a string.
For more list automation from the console, my PnP PowerShell scripts for SharePoint developers collection is a good next stop.
If you’re moving older SharePoint code to the modern stack, these will help:
Frequently Asked Questions
What is an event receiver in SharePoint?
Code SharePoint runs when an item, list or site changes. -ing events (ItemAdding) run before the save and can cancel it; -ed events run after. Classic receivers are SharePoint Server farm code.
Can I use event receivers in SharePoint Online?
Not server-side ones. Remote event receivers registered with ACS stopped on April 2, 2026, and Entra-registered ones stop on July 1, 2027. Use webhooks or Power Automate instead.
How long does a SharePoint webhook subscription last?
Up to 180 days. Set expirationDateTime when you create it and renew it before it expires with a PATCH or Set-PnPWebhookSubscription.
Does a SharePoint webhook tell me which item changed?
No. The notification has the subscription, list and site IDs only. Call GetChanges with your last change token to get the items, as Microsoft describes in its SharePoint webhooks overview.
Can a webhook cancel a change like ItemAdding did?
No. Webhooks are asynchronous only. To protect content, use permissions or a library users can’t edit.

Hey! I’m Bijay Kumar, founder of SPGuides.com and a Microsoft Business Applications MVP (Power Automate, Power Apps). I launched this site in 2020 because I truly enjoy working with SharePoint, Power Platform, and SharePoint Framework (SPFx), and wanted to share that passion through step-by-step tutorials, guides, and training videos. My mission is to help you learn these technologies so you can utilize SharePoint, enhance productivity, and potentially build business solutions along the way.