# Guide to Microsoft SharePoint Webhooks: Features and Best Practices

Microsoft SharePoint webhooks notify your systems when items change in a list or document library. If you're building on SharePoint, webhooks are how you learn a list changed without polling, though you then call the API to find out what changed.

This guide covers how SharePoint webhooks work, the validation handshake (there's no signature), the thin notifications and the GetChanges pattern, subscription renewal, and the best practices for production.

## What are Microsoft SharePoint webhooks?

SharePoint list webhooks send an HTTP POST to a `notificationUrl` you register per list. There's no HMAC or request signature. Authenticity rests on two things: a mandatory validationtoken echo when the subscription is created, and an optional clientState string echoed in every notification.

Notifications are thin, they tell you a list changed but carry no change details, so you call the list's GetChanges API with a stored change token to learn what happened.

## Microsoft SharePoint webhook features

| Feature | Details |
| --- | --- |
| Configuration | `POST /_api/web/lists('{list-guid}')/subscriptions` |
| Validation | Echo the `validationtoken` query param as text/plain 200 within a few seconds |
| Per-message check | Optional `clientState` echoed in notifications (shared-secret sanity check) |
| Signature | None |
| Payload | Thin, batched under `value[]`: `subscriptionId`, `clientState`, `resource` (list GUID), ... no change detail |
| Learn what changed | Call GetChanges with a stored change token |
| Expiry | Max 180 days; must be renewed via `PATCH` |
| Retries | 5x at 5-minute intervals on non-2xx |
| SDK | `@pnp/sp` (manages subscriptions, not verification) |

## Common events

Notifications do not carry an event type. The underlying list events are:

| Event | Fires when |
| --- | --- |
| `ItemAdded` | A list item / document is added |
| `ItemUpdated` | A list item / document is updated |
| `ItemDeleted` | A list item / document is deleted |

Because the notification only says "this list changed", you determine the specific change by calling GetChanges.

## Setting up Microsoft SharePoint webhooks

Create a subscription with `POST /_api/web/lists('{list-guid}')/subscriptions`, providing `resource`, `notificationUrl`, `expirationDateTime` (max 180 days out), and an optional `clientState`. SharePoint validates the endpoint with the handshake below before creating the subscription. Renew via `PATCH` before expiry, SharePoint doesn't track usage, so an un-renewed subscription simply lapses.

## Securing Microsoft SharePoint webhooks

There's no signature, so verification is two steps.

### The validation handshake

When a subscription is created (or its `notificationUrl` changes), SharePoint POSTs with a `validationtoken` query-string parameter. Echo that exact token back as an HTTP 200 `text/plain` body within a few seconds, or creation fails.

### clientState on notifications

Set a `clientState` at subscription time; SharePoint echoes it in every notification's `clientState` field. Compare it to your stored value as a shared-secret sanity check.

```javascript
const CLIENT_STATE = process.env.SP_CLIENT_STATE;

app.post("/webhook", express.json(), (req, res) => {
  // Validation handshake: echo the token as text/plain
  const validationToken = req.query.validationtoken;
  if (validationToken) {
    return res.status(200).type("text/plain").send(validationToken);
  }

  // Notifications arrive batched under value[]
  const notifications = (req.body && req.body.value) || [];
  for (const n of notifications) {
    if (CLIENT_STATE && n.clientState !== CLIENT_STATE) {
      return res.sendStatus(400); // not from your subscription
    }
  }

  res.sendStatus(200); // acknowledge fast (SharePoint retries 5x on non-2xx)
  for (const n of notifications) {
    // Enqueue: call GetChanges on n.resource with your stored change token
    processQueue.add(n);
  }
});

```

Because there's no cryptographic signature, set `clientState`, keep the `notificationUrl` secret, and use HTTPS.

## Microsoft SharePoint webhook limitations and pain points

### No signature, only clientState

The Problem: There's no HMAC to verify. The only per-message identity signal is the optional `clientState`. Without it, any POST to your URL looks legitimate.

Why It Happens: SharePoint's model is a validation handshake plus an optional shared-state string, not payload signing.

Workarounds:

* Set `clientState` and check it, keep the URL secret, and use HTTPS.

How Hookdeck Can Help: Hookdeck gives you a dedicated ingestion URL and can enforce its own verification in front of your endpoint, adding a layer beyond `clientState`.

### Thin notifications require GetChanges

The Problem: Notifications carry no change detail, just that a list changed. You must call GetChanges with a stored change token to learn what happened, and manage that token per list.

Why It Happens: SharePoint notifies that something changed and expects you to pull the delta.

Workarounds:

* Store a change token per list, call GetChanges on notification, and advance the token.

How Hookdeck Can Help: Hookdeck durably queues each notification so your worker calls GetChanges at a controlled rate, without racing the 5-minute retry cadence.

### Subscriptions expire at 180 days

The Problem: A subscription expires at its `expirationDateTime` (max 180 days) and must be renewed via `PATCH`. Miss the renewal and notifications stop silently.

Why It Happens: SharePoint time-boxes subscriptions and doesn't auto-renew.

Workarounds:

* Track `expirationDateTime` and renew well before it, from a scheduled job.

How Hookdeck Can Help: Hookdeck's observability surfaces a drop in notification volume, so a lapsed subscription shows up as an alert rather than silent data loss.

### The validation window and retries

The Problem: You must echo the `validationtoken` within a few seconds or creation fails, and notifications retry only 5x at 5-minute intervals, so a longer outage drops them.

Why It Happens: SharePoint validates quickly and retries sparingly.

Workarounds:

* Echo the token immediately, acknowledge fast, and reconcile via GetChanges for anything missed.

How Hookdeck Can Help: Hookdeck can complete the handshake and durably queue notifications, so a downstream hiccup doesn't exhaust the small retry budget.

## Best practices

### Echo the validationtoken and check clientState

Return the `validationtoken` as text/plain 200 within a few seconds, and verify `clientState` on every notification.

### Drive off GetChanges with a stored token

Treat notifications as a nudge; call GetChanges with a per-list change token to learn and process the actual changes.

### Renew before 180 days

Track `expirationDateTime` and renew via `PATCH` ahead of expiry.

### Acknowledge fast, process asynchronously

Return 200 quickly and defer GetChanges to a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

SharePoint list webhooks have no signature: authenticity is the `validationtoken` handshake plus an optional `clientState`, and notifications are thin, so you call GetChanges with a stored change token to learn what changed. Set `clientState`, drive off GetChanges, renew before the 180-day expiry, and acknowledge fast.

[Hookdeck](https://hookdeck.com) completes the handshake, adds a verification layer SharePoint lacks, and durably queues every notification at the edge, so your app processes trustworthy events without losing any to a lapsed subscription.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Microsoft SharePoint webhooks reliably in minutes.