Skip to content

Commit 063d33f

Browse files
authored
Merge pull request #882 from ValerasNarbutas/neasample/881Webhookmaintenance
Add sample JSON metadata for the SPO Webhook Subscription Maintenance…
2 parents adbf090 + 092eb97 commit 063d33f

File tree

4 files changed

+453
-0
lines changed

4 files changed

+453
-0
lines changed
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
# Maintain and Replace SharePoint List Webhook Subscriptions
2+
3+
## Summary
4+
5+
This PowerShell script helps maintain and replace SharePoint list webhook subscriptions across multiple sites. It identifies existing webhook subscriptions, removes old webhook URLs, and replaces them with updated Power Automate webhook subscriptions. The script includes retry logic for reliability and exports detailed results to a CSV file.
6+
7+
![Example Screenshot](assets/example.png)
8+
9+
The script provides a comprehensive solution for managing webhook subscriptions in SharePoint, ensuring that organizations can keep their integrations up to date and functioning smoothly.
10+
11+
# [PnP PowerShell](#tab/pnpps)
12+
13+
```powershell
14+
15+
<#
16+
.SYNOPSIS
17+
Maintain and replace SharePoint list webhook subscriptions across multiple sites.
18+
19+
.DESCRIPTION
20+
This script checks SharePoint sites for a specific list, identifies existing webhook subscriptions,
21+
removes old webhook URLs, and replaces them with updated Power Automate webhook subscriptions.
22+
23+
Features:
24+
- Connects to multiple SharePoint sites
25+
- Identifies and removes old webhook URLs
26+
- Adds new webhook subscriptions with configurable expiration
27+
- Implements retry logic for reliability
28+
- Exports detailed results to CSV
29+
- Provides comprehensive logging
30+
31+
.PARAMETER SiteUrls
32+
Array of SharePoint site URLs to process. Can be provided directly or from a CSV file.
33+
34+
.PARAMETER ListName
35+
Name of the SharePoint list to check for webhooks (default: "Documents")
36+
37+
.PARAMETER OldWebhookUrl
38+
The old webhook URL that should be replaced (optional)
39+
40+
.PARAMETER NewWebhookUrl
41+
The new webhook URL to add
42+
43+
.PARAMETER ClientId
44+
Azure AD Application (Client) ID for authentication
45+
46+
.PARAMETER Tenant
47+
Tenant name (e.g., contoso.onmicrosoft.com)
48+
49+
.PARAMETER Thumbprint
50+
Certificate thumbprint for authentication (optional, if using certificate auth)
51+
52+
.PARAMETER ExpirationDays
53+
Number of days until webhook expiration (default: 179, max: 180)
54+
55+
.PARAMETER MaxRetries
56+
Maximum number of retry attempts for adding webhooks (default: 3)
57+
58+
.PARAMETER RetryDelaySeconds
59+
Delay in seconds between retry attempts (default: 5)
60+
61+
.PARAMETER OutputPath
62+
Path for the output CSV report (default: current directory)
63+
64+
.EXAMPLE
65+
.\script.ps1 -SiteUrls @("https://contoso.sharepoint.com/sites/site1", "https://contoso.sharepoint.com/sites/site2") `
66+
-ListName "Documents" `
67+
-NewWebhookUrl "https://prod.westeurope.logic.azure.com:443/workflows/abc123/triggers/manual/paths/invoke..." `
68+
-ClientId "12345678-1234-1234-1234-123456789abc" `
69+
-Tenant "contoso.onmicrosoft.com" `
70+
-Thumbprint "ABC123..."
71+
72+
.EXAMPLE
73+
# Using interactive authentication
74+
.\script.ps1 -SiteUrls @("https://contoso.sharepoint.com/sites/site1") `
75+
-ListName "Media" `
76+
-OldWebhookUrl "https://old-webhook-url.com/..." `
77+
-NewWebhookUrl "https://new-webhook-url.com/..."
78+
79+
.NOTES
80+
Author: Valeras Narbutas
81+
GitHub: ValerasNarbutas
82+
Requires: PnP PowerShell module
83+
#>
84+
85+
[CmdletBinding()]
86+
param(
87+
[Parameter(Mandatory = $true, HelpMessage = "Array of SharePoint site URLs to process")]
88+
[ValidateNotNullOrEmpty()]
89+
[string[]]$SiteUrls,
90+
91+
[Parameter(Mandatory = $false, HelpMessage = "Name of the SharePoint list")]
92+
[string]$ListName = "Documents",
93+
94+
[Parameter(Mandatory = $false, HelpMessage = "Old webhook URL to replace (optional)")]
95+
[string]$OldWebhookUrl,
96+
97+
[Parameter(Mandatory = $true, HelpMessage = "New webhook URL to add")]
98+
[ValidateNotNullOrEmpty()]
99+
[string]$NewWebhookUrl,
100+
101+
[Parameter(Mandatory = $false, HelpMessage = "Azure AD Application (Client) ID")]
102+
[string]$ClientId,
103+
104+
[Parameter(Mandatory = $false, HelpMessage = "Tenant name (e.g., contoso.onmicrosoft.com)")]
105+
[string]$Tenant,
106+
107+
[Parameter(Mandatory = $false, HelpMessage = "Certificate thumbprint for authentication")]
108+
[string]$Thumbprint,
109+
110+
[Parameter(Mandatory = $false, HelpMessage = "Number of days until webhook expiration")]
111+
[ValidateRange(1, 180)]
112+
[int]$ExpirationDays = 179,
113+
114+
[Parameter(Mandatory = $false, HelpMessage = "Maximum number of retry attempts")]
115+
[ValidateRange(1, 10)]
116+
[int]$MaxRetries = 3,
117+
118+
[Parameter(Mandatory = $false, HelpMessage = "Delay in seconds between retries")]
119+
[ValidateRange(1, 60)]
120+
[int]$RetryDelaySeconds = 5,
121+
122+
[Parameter(Mandatory = $false, HelpMessage = "Output path for CSV report")]
123+
[string]$OutputPath = "."
124+
)
125+
126+
# Initialize client state with timestamp
127+
$clientState = "WebhookUpdate-" + (Get-Date -Format "yyyyMMdd-HHmmss")
128+
129+
# Initialize results array
130+
$results = @()
131+
132+
Write-Host "========================================" -ForegroundColor Cyan
133+
Write-Host "SharePoint Webhook Subscription Maintenance" -ForegroundColor Cyan
134+
Write-Host "========================================" -ForegroundColor Cyan
135+
Write-Host ""
136+
Write-Host "Configuration:" -ForegroundColor Yellow
137+
Write-Host " - List Name: $ListName" -ForegroundColor Gray
138+
Write-Host " - Sites to process: $($SiteUrls.Count)" -ForegroundColor Gray
139+
Write-Host " - Expiration days: $ExpirationDays" -ForegroundColor Gray
140+
Write-Host " - Max retries: $MaxRetries" -ForegroundColor Gray
141+
Write-Host ""
142+
143+
# Process each site
144+
foreach ($siteUrl in $SiteUrls) {
145+
Write-Host "Processing site: $siteUrl" -ForegroundColor Cyan
146+
147+
try {
148+
# Connect to site using appropriate authentication method
149+
if ($ClientId -and $Tenant -and $Thumbprint) {
150+
Write-Host " - Connecting with certificate authentication..." -ForegroundColor Gray
151+
Connect-PnPOnline -Url $siteUrl -ClientId $ClientId -Tenant $Tenant -Thumbprint $Thumbprint
152+
}
153+
elseif ($ClientId -and $Tenant) {
154+
Write-Host " - Connecting with interactive authentication..." -ForegroundColor Gray
155+
Connect-PnPOnline -Url $siteUrl -ClientId $ClientId -Tenant $Tenant -Interactive
156+
}
157+
else {
158+
Write-Host " - Connecting with interactive authentication..." -ForegroundColor Gray
159+
Connect-PnPOnline -Url $siteUrl -Interactive
160+
}
161+
162+
# Check if the specified list exists
163+
$list = Get-PnPList -Identity $ListName -ErrorAction Stop
164+
165+
if ($null -ne $list) {
166+
Write-Host " - List '$ListName' found" -ForegroundColor Green
167+
168+
# Get existing webhooks for the list
169+
$webhooks = Get-PnPWebhookSubscriptions -List $ListName
170+
171+
# Initialize tracking variables
172+
$oldWebhookFound = $false
173+
$oldWebhookRemoved = $false
174+
$newWebhookAdded = $false
175+
$otherWebhooksCount = 0
176+
$removedWebhookId = ""
177+
$newWebhookId = ""
178+
$newWebhookExpiration = ""
179+
$newWebhookAlreadyExists = $false
180+
$retryCount = 0
181+
$webhookError = ""
182+
183+
if ($webhooks.Count -gt 0) {
184+
Write-Host " - Found $($webhooks.Count) existing webhook(s)" -ForegroundColor Gray
185+
186+
# Check existing webhooks
187+
foreach ($webhook in $webhooks) {
188+
# Check for old webhook URL (if specified)
189+
if ($OldWebhookUrl -and $webhook.NotificationUrl -eq $OldWebhookUrl) {
190+
$oldWebhookFound = $true
191+
$removedWebhookId = $webhook.Id
192+
193+
Write-Host " - Found old webhook, removing..." -ForegroundColor Yellow
194+
Remove-PnPWebhookSubscription -List $ListName -Identity $webhook.Id -Force
195+
$oldWebhookRemoved = $true
196+
Write-Host " Removed webhook ID: $($webhook.Id)" -ForegroundColor Gray
197+
}
198+
# Check if new webhook already exists
199+
elseif ($webhook.NotificationUrl -eq $NewWebhookUrl) {
200+
$newWebhookAlreadyExists = $true
201+
$newWebhookId = $webhook.Id
202+
$newWebhookExpiration = $webhook.ExpirationDateTime
203+
Write-Host " - New webhook already exists (ID: $($webhook.Id))" -ForegroundColor Green
204+
}
205+
else {
206+
$otherWebhooksCount++
207+
Write-Host " - Found other webhook: $($webhook.Id)" -ForegroundColor Gray
208+
}
209+
}
210+
}
211+
else {
212+
Write-Host " - No existing webhooks found" -ForegroundColor Gray
213+
}
214+
215+
# Add new webhook if it doesn't already exist (with retry logic)
216+
if (-not $newWebhookAlreadyExists) {
217+
$webhookAddSuccess = $false
218+
219+
for ($retryCount = 0; $retryCount -lt $MaxRetries; $retryCount++) {
220+
try {
221+
if ($retryCount -gt 0) {
222+
Write-Host " - Retry attempt $retryCount of $($MaxRetries - 1)..." -ForegroundColor Yellow
223+
Start-Sleep -Seconds $RetryDelaySeconds
224+
}
225+
else {
226+
Write-Host " - Adding new webhook..." -ForegroundColor Yellow
227+
}
228+
229+
# Set expiration date
230+
$expirationDate = (Get-Date).AddDays($ExpirationDays).ToUniversalTime()
231+
232+
# Add webhook subscription
233+
$newWebhook = Add-PnPWebhookSubscription -List $ListName `
234+
-NotificationUrl $NewWebhookUrl `
235+
-ExpirationDate $expirationDate `
236+
-ClientState $clientState
237+
238+
$newWebhookAdded = $true
239+
$newWebhookId = $newWebhook.Id
240+
$newWebhookExpiration = $newWebhook.ExpirationDateTime
241+
$webhookAddSuccess = $true
242+
Write-Host " - New webhook created successfully!" -ForegroundColor Green
243+
Write-Host " Webhook ID: $newWebhookId" -ForegroundColor Gray
244+
Write-Host " Expires: $newWebhookExpiration" -ForegroundColor Gray
245+
break
246+
}
247+
catch {
248+
$webhookError = $_.Exception.Message
249+
Write-Host " - Failed to add webhook (attempt $($retryCount + 1)): $webhookError" -ForegroundColor Red
250+
251+
if ($retryCount -eq ($MaxRetries - 1)) {
252+
Write-Host " - Max retries reached. Webhook addition failed." -ForegroundColor Red
253+
}
254+
}
255+
}
256+
}
257+
258+
# Determine action taken
259+
$action = "No action"
260+
if ($oldWebhookRemoved -and $newWebhookAdded) {
261+
$action = "Replaced old webhook"
262+
}
263+
elseif ($oldWebhookRemoved -and $newWebhookAlreadyExists) {
264+
$action = "Removed old webhook (new already existed)"
265+
}
266+
elseif ($newWebhookAdded -and -not $oldWebhookFound) {
267+
$action = "Added new webhook"
268+
}
269+
elseif ($newWebhookAlreadyExists -and -not $oldWebhookFound) {
270+
$action = "New webhook already exists"
271+
}
272+
elseif (-not $newWebhookAlreadyExists -and -not $newWebhookAdded) {
273+
$action = "Failed to add webhook after retries"
274+
}
275+
276+
Write-Host " - Action: $action" -ForegroundColor $(if ($action -like "*Failed*" -or $action -eq "No action") { "Yellow" } else { "Green" })
277+
278+
# Add to results
279+
$results += [PSCustomObject]@{
280+
SiteUrl = $siteUrl
281+
ListName = $ListName
282+
Action = $action
283+
OldWebhookFound = $oldWebhookFound
284+
OldWebhookRemoved = $oldWebhookRemoved
285+
RemovedWebhookId = if ($removedWebhookId) { $removedWebhookId } else { "N/A" }
286+
NewWebhookAdded = $newWebhookAdded
287+
NewWebhookAlreadyExists = $newWebhookAlreadyExists
288+
NewWebhookId = if ($newWebhookId) { $newWebhookId } else { "N/A" }
289+
NewWebhookExpiration = if ($newWebhookExpiration) { $newWebhookExpiration } else { "N/A" }
290+
OtherWebhooksCount = $otherWebhooksCount
291+
RetryAttempts = $retryCount
292+
ClientState = $clientState
293+
UpdatedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
294+
WebhookError = if ($webhookError) { $webhookError } else { "N/A" }
295+
}
296+
}
297+
else {
298+
Write-Host " - List '$ListName' not found in this site" -ForegroundColor Yellow
299+
300+
$results += [PSCustomObject]@{
301+
SiteUrl = $siteUrl
302+
ListName = $ListName
303+
Action = "List not found"
304+
OldWebhookFound = $false
305+
OldWebhookRemoved = $false
306+
RemovedWebhookId = "N/A"
307+
NewWebhookAdded = $false
308+
NewWebhookAlreadyExists = $false
309+
NewWebhookId = "N/A"
310+
NewWebhookExpiration = "N/A"
311+
OtherWebhooksCount = 0
312+
RetryAttempts = 0
313+
ClientState = "N/A"
314+
UpdatedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
315+
WebhookError = "List '$ListName' not found"
316+
}
317+
}
318+
}
319+
catch {
320+
$errorMessage = $_.Exception.Message
321+
Write-Host " - Failed to process site: $errorMessage" -ForegroundColor Red
322+
323+
$results += [PSCustomObject]@{
324+
SiteUrl = $siteUrl
325+
ListName = $ListName
326+
Action = "Error"
327+
OldWebhookFound = $false
328+
OldWebhookRemoved = $false
329+
RemovedWebhookId = "N/A"
330+
NewWebhookAdded = $false
331+
NewWebhookAlreadyExists = $false
332+
NewWebhookId = "N/A"
333+
NewWebhookExpiration = "N/A"
334+
OtherWebhooksCount = 0
335+
RetryAttempts = 0
336+
ClientState = "N/A"
337+
UpdatedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
338+
WebhookError = $errorMessage
339+
}
340+
}
341+
finally {
342+
# Disconnect to avoid connection limits
343+
try {
344+
Disconnect-PnPOnline -ErrorAction SilentlyContinue
345+
}
346+
catch {
347+
# Ignore disconnect errors
348+
}
349+
}
350+
351+
Write-Host ""
352+
}
353+
354+
# Export results to CSV
355+
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
356+
$outputFile = Join-Path $OutputPath "WebhookUpdate_$timestamp.csv"
357+
$results | Export-Csv -Path $outputFile -NoTypeInformation -Encoding UTF8
358+
359+
# Display summary
360+
Write-Host "========================================" -ForegroundColor Cyan
361+
Write-Host "Summary Report" -ForegroundColor Cyan
362+
Write-Host "========================================" -ForegroundColor Cyan
363+
Write-Host ""
364+
Write-Host "Results exported to: $outputFile" -ForegroundColor Green
365+
Write-Host ""
366+
Write-Host "Statistics:" -ForegroundColor Yellow
367+
Write-Host " Total sites processed: $($results.Count)" -ForegroundColor White
368+
Write-Host " Old webhooks replaced: $(($results | Where-Object { $_.Action -eq 'Replaced old webhook' }).Count)" -ForegroundColor Green
369+
Write-Host " New webhooks added: $(($results | Where-Object { $_.Action -eq 'Added new webhook' }).Count)" -ForegroundColor Green
370+
Write-Host " New webhook already existed: $(($results | Where-Object { $_.Action -eq 'New webhook already exists' }).Count)" -ForegroundColor Yellow
371+
Write-Host " Old webhook removed (new existed): $(($results | Where-Object { $_.Action -eq 'Removed old webhook (new already existed)' }).Count)" -ForegroundColor Yellow
372+
Write-Host " List not found: $(($results | Where-Object { $_.Action -eq 'List not found' }).Count)" -ForegroundColor Yellow
373+
Write-Host " Failed after retries: $(($results | Where-Object { $_.Action -eq 'Failed to add webhook after retries' }).Count)" -ForegroundColor Red
374+
Write-Host " Errors: $(($results | Where-Object { $_.Action -eq 'Error' }).Count)" -ForegroundColor Red
375+
Write-Host ""
376+
Write-Host "========================================" -ForegroundColor Cyan
377+
378+
```
379+
[!INCLUDE [More about PnP PowerShell](../../docfx/includes/MORE-PNPPS.md)]
380+
***
381+
382+
383+
## Contributors
384+
385+
| Author(s) |
386+
|-----------|
387+
| [Valeras Narbutas](https://github.com/ValerasNarbutas) |
388+
389+
390+
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
391+
<img src="https://m365-visitor-stats.azurewebsites.net/script-samples/scripts/spo-webhook-subscription-maintenance" aria-hidden="true" />
41.1 KB
Loading
41.1 KB
Loading

0 commit comments

Comments
 (0)