Agent skill
Microsoft SharePoint Webhooks Skill
Receive and verify Microsoft SharePoint webhooks. Use when setting up SharePoint list/document-library webhook handlers, completing the validationtoken handshake, validating clientState, or reacting to list-item changes like ItemAdded/ItemUpdated by calling the GetChanges API.
Install this skill
npx skills add hookdeck/webhook-skills --skill microsoft-sharepoint-webhooks
When to Use This Skill
- Setting up Microsoft SharePoint list or document-library webhook handlers
- Completing the SharePoint
validationtokensubscription handshake - Validating the
clientStateshared secret on incoming notifications - Understanding the thin SharePoint notification payload
- Reacting to list-item changes (ItemAdded, ItemUpdated, ItemDeleted) via the GetChanges API
How SharePoint Webhooks Differ
SharePoint webhooks are not HMAC-signed and are not Standard Webhooks. There is no request signature. Authenticity relies on two things instead:
- Validation handshake — when a subscription is created (or its
notificationUrlchanges), SharePoint POSTs with avalidationtokenquery-string parameter. Your endpoint must echo that exact token back as an HTTP200text/plainbody within ~5 seconds, or the subscription is never created. clientState— an opaque string you set at subscription time. SharePoint echoes it in theclientStatefield of every notification. Compare it to your stored secret as a shared-secret sanity check. It is the only per-message identity signal (not a signature).
Notifications are thin and batched under a value array and carry no change details — you call the list GetChanges API with a stored change token to learn what actually changed.
Verification (core)
const crypto = require('crypto');
// 1. Validation handshake — runs BEFORE any body parsing.
// Echo the validationtoken query param back verbatim as text/plain.
const token = new URL(req.url, 'http://localhost').searchParams.get('validationtoken');
if (token) {
res.setHeader('Content-Type', 'text/plain');
return res.status(200).send(token); // must reply within ~5s or creation fails
}
// 2. clientState — timing-safe compare the shared secret on every notification.
function clientStateMatches(received, expected) {
if (typeof received !== 'string' || typeof expected !== 'string') return false;
const a = Buffer.from(received);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
const { value = [] } = JSON.parse(rawBody); // thin, batched notifications
const ok = value.every(n => clientStateMatches(n.clientState, process.env.SHAREPOINT_CLIENT_STATE));
if (!ok) return res.status(400).send('Invalid clientState');
// Notifications carry no change details — call list GetChanges with your stored change token.
For complete handlers with route wiring, GetChanges follow-up, and tests, see:
Notification Payload
Each notification in the batch has this shape (no change details):
{
"value": [
{
"subscriptionId": "91779246-afe9-4525-b122-6c199ae89211",
"clientState": "your-opaque-secret",
"expirationDateTime": "2016-04-30T17:27:00.0000000Z",
"resource": "b9f6f714-9df8-470b-b22e-653855e1c181",
"tenantId": "00000000-0000-0000-0000-000000000000",
"siteUrl": "/",
"webId": "dbc5a806-e4d4-46e5-951c-6344d70b62fa"
}
]
}
resource is the list GUID. To learn what changed, call GetChanges on that list.
Change Types (from GetChanges)
The notification does not carry the event type. After a notification you call GetChanges and inspect each change's ChangeType:
| ChangeType | List event | Triggered when |
|---|---|---|
Add | ItemAdded | An item or file is created |
Update | ItemUpdated | An item or file is modified |
DeleteObject | ItemDeleted | An item or file is deleted |
Rename | ItemRenamed | An item or file is renamed |
Restore | ItemRestored | An item is restored from the recycle bin |
MoveAway | ItemMovedOut | An item or file is moved out of the location |
MoveInto | ItemMovedInto | An item or file is moved into the location |
For the full change reference, see SharePoint list webhooks.
Environment Variables
SHAREPOINT_CLIENT_STATE=your-opaque-secret # clientState set at subscription time
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 microsoft-sharepoint --path /webhooks/microsoft-sharepoint
Reference Materials
- references/overview.md - SharePoint webhook concepts, change types
- references/setup.md - Creating and renewing subscriptions
- references/verification.md - Handshake and clientState details