Webhook Channels & Customization
Send PIM Monitor notifications to Teams, Slack, Discord, or custom webhooks.
Quick Setup
- Get webhook URL from your platform (Teams, Slack, Discord)
- Set the variable:
NOTIFICATION_WEBHOOK_URL = https://... - Run the pipeline — scan results appear in your chat
PIM Monitor automatically detects the webhook type by URL pattern.
Supported Channels
Microsoft Teams (Power Automate)
URL pattern: webhook.office.com
Setup:
- In Teams, go to the channel where you want notifications
- Click [...] → Connectors → Configure
- Search Power Automate → Configure
- Give it a name: "PIM Monitor"
- Create → Copy the webhook URL
Payload format: Adaptive Card
Example:
┌─────────────────────────────────┐
│ PIM Monitor │
│ 2 High, 1 Medium changes │
│ │
│ ■ Directory Roles (High) │
│ Global Administrator > policy │
│ │
│ ■ Auth Contexts (Medium) │
│ Conditional Access rule │
│ │
│ [View diff] 2026-04-27T18:42Z │
└─────────────────────────────────┘
Slack
URL pattern: hooks.slack.com
Setup:
- Go to api.slack.com → Apps → Create New App
- From scratch → Name: "PIM Monitor", workspace: select yours
- Incoming Webhooks → Turn on
- Add New Webhook to Workspace → Select channel → Allow
- Copy the webhook URL
Payload format: Slack blocks (with markdown, colors)
Example:
📊 PIM Monitor
━━━━━━━━━━━━━━━━━━━━━━━━━
2 High, 1 Medium changes
🔴 High (2)
• Directory Roles > policy change
• Auth Contexts > rule update
🟠 Medium (1)
• PIM Groups > expiration adjusted
[View diff] 2026-04-27 18:42 UTC
Discord
URL pattern: discord.com/api/webhooks
Setup:
- In Discord, go to Server Settings → Integrations → Webhooks
- Create Webhook
- Name: "PIM Monitor"
- Select channel: where notifications appear
- Copy Webhook URL
Payload format: Discord embed (with colors, timestamps)
Example:
╔═════════════════════════════════╗
║ PIM Monitor ║
║ Scan completed: 3 changes ║
╠═════════════════════════════════╣
║ 🔴 High (2) ║
║ • role policy change ║
║ ║
║ 🟠 Medium (1) ║
║ • expiration update ║
║ ║
║ 2026-04-27 18:42:15 UTC ║
╚═════════════════════════════════╝
Generic JSON (Custom Webhooks)
URL pattern: Any URL NOT matching Teams/Slack/Discord
For custom APIs, webhooks, or other platforms.
Payload format: Plain JSON
{
"text": "[PIM Monitor] 3 changes detected",
"summary": "2 High, 1 Medium",
"changesBySeverity": {
"High": [
{
"workload": "directory-roles",
"entity": "global-administrator",
"description": "policy updated",
"severity": "High"
}
],
"Medium": [...],
"Low": [...],
"Informational": [...]
}
}
How Auto-Detection Works
The Get-WebhookType function (notifications.ps1) inspects the URL:
function Get-WebhookType {
param([string] $Url)
if ($Url -match "webhook\.office\.com") {
return "Teams"
}
elseif ($Url -match "hooks\.slack\.com") {
return "Slack"
}
elseif ($Url -match "discord\.com/api/webhooks") {
return "Discord"
}
else {
return "Generic"
}
}
To detect a custom service, modify this function to add a new URL pattern:
elseif ($Url -match "my-custom-api\.com") {
return "MyCustom"
}
Customizing Payloads
Teams Adaptive Card
Edit Build-TeamsPayload in src/notifications.ps1 (lines ~837–1000):
function Build-TeamsPayload {
param($ChangesBySeverity)
$card = @{
'$schema' = 'http://adaptivecards.io/schemas/adaptive-card.json'
type = 'AdaptiveCard'
version = '1.5'
body = @(
@{ type = 'TextBlock'; text = 'My Custom Title'; size = 'Large' }
# ... add or remove blocks here
)
}
return @{
type = 'message'
attachments = @(@{
contentType = 'application/vnd.microsoft.card.adaptive'
content = $card
})
}
}
Adaptive Card reference: https://adaptivecards.io/
Common customizations:
- Add
backgroundColorto style containers - Add
actionbuttons (Open URL, Submit action) - Change
factSetlayout (currently lists High/Medium/Low summary) - Add images with
Imageblocks
Slack Blocks
Edit Build-SlackPayload in src/notifications.ps1 (lines ~1009–1117):
function Build-SlackPayload {
param($ChangesBySeverity)
$blocks = @(
@{ type = 'header'; text = @{ type = 'plain_text'; text = 'My Title' } }
@{ type = 'section'; text = @{ type = 'mrkdwn'; text = 'Custom markdown here' } }
# ... build blocks
)
return @{ blocks = $blocks }
}
Slack blocks reference: https://api.slack.com/block-kit
Common customizations:
- Add
dividerblocks between sections - Add
contextblocks for metadata/timestamps - Add
buttonelements withaction_idandvalue - Use
imageblocks for logos/diagrams
Discord Embeds
Edit Build-DiscordPayload in src/notifications.ps1 (lines ~1126–1236):
function Build-DiscordPayload {
param($ChangesBySeverity)
$embed = @{
title = 'My Title'
description = 'Description here'
color = 16711680 # Red in decimal (0xFF0000)
fields = @(...)
thumbnail = @{ url = 'https://example.com/image.png' }
footer = @{ text = 'Scan completed' }
}
return @{ embeds = @($embed) }
}
Discord embed reference: https://discord.com/developers/docs/resources/channel#embed-object
Common customizations:
- Change
color(decimal format: 16711680 = red, 65280 = green) - Add
thumbnailorimageURLs - Add
footertext - Set
timestampto ISO 8601 UTC
Generic JSON
Edit the fallback case in Send-WebhookNotification:
default {
@{
text = "[PIM Monitor] Scan completed"
changeCount = $ChangesBySeverity.Total
high = $ChangesBySeverity.High.Count
medium = $ChangesBySeverity.Medium.Count
low = $ChangesBySeverity.Low.Count
# ... add any custom fields
}
}
Adding a Custom Webhook Channel
Step 1: Create a Payload Builder
function Build-MyCustomPayload {
param($ChangesBySeverity)
@{
title = "PIM Monitor Scan"
changes = $ChangesBySeverity.Total
severity = @{
high = $ChangesBySeverity.High.Count
medium = $ChangesBySeverity.Medium.Count
low = $ChangesBySeverity.Low.Count
}
changes_high = @($ChangesBySeverity.High | ForEach-Object { $_.description })
}
}
Step 2: Add URL Detection
In Get-WebhookType, add:
elseif ($Url -match "my-service\.com") {
return "MyService"
}
Step 3: Add Case to Dispatcher
In Send-WebhookNotification, add:
$payload = switch ($type) {
'Teams' { Build-TeamsPayload -ChangesBySeverity $ChangesBySeverity }
'Slack' { Build-SlackPayload -ChangesBySeverity $ChangesBySeverity }
'Discord' { Build-DiscordPayload -ChangesBySeverity $ChangesBySeverity }
'MyService' { Build-MyCustomPayload -ChangesBySeverity $ChangesBySeverity }
default { Build-MyCustomPayload -ChangesBySeverity $ChangesBySeverity }
}
Step 4: Test
Set NOTIFICATION_WEBHOOK_URL to your service's webhook URL and run the pipeline.
Testing Webhooks Locally
Using curl
# Test a Teams webhook (replace with your actual URL)
curl -X POST https://outlook.webhook.office.com/webhookb2/... \
-H 'Content-Type: application/json' \
-d '{
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{"type": "TextBlock", "text": "Test Message"}
]
}
}]
}'
Using PowerShell
$payload = @{
type = "message"
attachments = @(@{
contentType = "application/vnd.microsoft.card.adaptive"
content = @{
type = "AdaptiveCard"
version = "1.5"
body = @(
@{ type = 'TextBlock'; text = 'Test from PowerShell' }
)
}
})
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $webhookUrl -Method Post -ContentType 'application/json' -Body $payload
Using Postman
- Create new POST request
- URL: Your webhook URL
- Headers:
Content-Type: application/json - Body (raw): Paste your payload JSON
- Send
Example Teams payload for Postman:
{
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"size": "Large",
"weight": "Bolder",
"text": "Test: PIM Monitor"
}
]
}
}]
}
Webhook Payload Size Limits
Different platforms have different size limits:
| Platform | Limit | Strategy |
|---|---|---|
| Teams | ~28 KB | Truncate long changes to first 15 per severity |
| Slack | ~3 MB | Truncate long changes to first 20 per severity |
| Discord | 2000 chars/field | Truncate error messages to ~200 chars |
| Generic | Unlimited (depends on endpoint) | No truncation |
Payloads that exceed limits are automatically truncated with "... [more]" indicators.
Troubleshooting Webhooks
Webhook not receiving messages
Check:
- Is webhook URL correct? Copy directly from platform (no typos)
- Is endpoint still valid? Some platforms disable old webhooks
- Check firewall/proxy not blocking outbound HTTPS
- Look at workflow logs for HTTP error codes
Common errors:
404 Not Found→ Invalid webhook URL401 Unauthorized→ Token/signature invalid403 Forbidden→ Webhook disabled or revoked429 Too Many Requests→ Rate limiting; try again later
Wrong message format
Check:
- Did
Get-WebhookTypecorrectly detect your platform? - Add debug logging:
Write-Host "Webhook type detected: $type" - Is payload builder correct? Try testing locally with curl first
Payload too large
Check:
- How many changes were detected? Many changes = larger payload
- Try filtering by
NOTIFICATION_MIN_SEVERITYto reduce message size - Use
REPORT_ARTIFACT=truefor detailed info (instead of including all in webhook)
Related Pages
- Environment Variables — NOTIFICATION_WEBHOOK_URL
- Email Notifications — Email setup
- Notifications — General notification configuration
- Scan Errors — Scan error webhook format