# Webhooks

> Signed publish and revalidation calls to your destination site. HMAC signing, payload schema, delivery status.

Webhooks let Mentionwell call your destination site when a post publishes. There are two optional endpoint types:

- **Publish endpoint** — receives the full post payload in `webhook_push` or `direct_api` delivery mode.
- **Revalidate endpoint** — receives a small cache-refresh payload after a successful publish push when `autoRevalidate` is enabled.

> Webhooks are optional. In pull mode (`api_reader` / `dynamic_reader` architecture in the [Connect wizard](/docs/connect-wizard)), your destination can ignore webhooks and fetch new posts from the Reader API on its own ISR/SWR interval.

## When Mentionwell fires

| Delivery setting | Path Mentionwell POSTs to | Payload |
|---|---|---|
| `publishEndpoint` in `webhook_push` mode | Your configured publish endpoint | `post.published` or `post.updated` plus the full `post` object |
| `publishEndpoint` in `direct_api` mode | Your configured publish endpoint | `{ siteSlug, post }` (no event wrapper) |
| `revalidateEndpoint` with `autoRevalidate: true` | Your configured revalidate endpoint | `post.revalidate`, `post.updated`, or `post.deleted`, a small post summary, and `paths` to refresh |
| Wizard **Test & save** | Your configured publish endpoint | `post.test` event with a stub post; `X-MentionWell-Test: 1` header set |

Set these by opening the [Connect-destination wizard](/docs/connect-wizard) — it pre-fills your secret and validates the connection. Manual config lives in **Integration → Reconfigure delivery** for advanced cases.

## Request headers Mentionwell sends

Every webhook (publish, revalidate, or test) includes:

| Header | Value | Notes |
|---|---|---|
| `Content-Type` | `application/json` | |
| `Authorization` | `Bearer <readApiKey>` | Same `mw_read_...` key your destination uses to read the API. Optional to verify; the signature is the security boundary. |
| `X-MentionWell-Signature` | hex HMAC-SHA256 of the raw body using your per-site `whsec_...` | **Required to verify before trusting the payload.** |
| `Idempotency-Key` | `sha1(siteId:slug:updatedAt)` | Only on `webhook_push` and `direct_api` publish events. Use to de-duplicate retries. |
| `X-MentionWell-Test` | `1` | Only on test pings fired by the dashboard wizard. Skip side effects when you see this. |

## Publish payload

```json
{
  "event": "post.published",
  "siteSlug": "your-site-slug",
  "post": {
    "slug": "five-questions-to-ask-before-you-buy",
    "title": "Five Questions to Ask Before You Buy",
    "html": "<article>...</article>",
    "markdown": "# Five Questions...",
    "publishedAt": "2026-04-21T15:00:00.000Z"
  },
  "sentAt": "2026-04-21T15:01:00.000Z"
}
```

`event` is `post.published` when the post is currently published and `post.updated` otherwise. Deletes are delivered to the revalidate endpoint as `post.deleted` (with the same `paths` array as a normal revalidate) so the destination can purge the post URL and refresh the listing.

The full `post` object in publish payloads includes everything the Reader API `GET .../posts/<slug>` endpoint returns: `metaTitle`, `metaDescription`, `excerpt`, `html`, `markdown`, `featuredImage`, `readingTime`, `tags`, `category`, `tldr`, `toc`, `faqs`, `canonicalUrl`, `schema.jsonLd`, and the timestamps. The example above shows only the most-used fields — your destination receives the full record.

### Test ping (from the Connect wizard)

When you click **Save & test** in the wizard, Mentionwell fires a single test event so you can verify the receiver is wired up before any real publish lands:

```json
{
  "event": "post.test",
  "siteSlug": "your-site-slug",
  "sentAt": "2026-05-18T12:00:00.000Z",
  "post": {
    "slug": "mentionwell-connection-test",
    "title": "Mentionwell connection test",
    "html": "<p>This is a test ping — it should be ignored, not published.</p>",
    "markdown": "This is a test ping.",
    "publishedAt": "2026-05-18T12:00:00.000Z"
  }
}
```

The test ping also sets `X-MentionWell-Test: 1`. Your receiver should treat `event === "post.test"` or that header as a connection check and respond `2xx` without writing the post anywhere.

## Revalidate payload

```json
{
  "event": "post.revalidate",
  "siteSlug": "your-site-slug",
  "post": {
    "slug": "five-questions-to-ask-before-you-buy",
    "title": "Five Questions to Ask Before You Buy",
    "publishedAt": "2026-04-21T15:00:00.000Z"
  },
  "paths": ["/blog", "/blog/five-questions-to-ask-before-you-buy"]
}
```

Always parse the body as raw text first (you need the exact bytes for signature verification), then `JSON.parse` it.

## Signature

Every webhook request includes the Mentionwell signature header:

```text
X-MentionWell-Signature: <hex hmac-sha256(rawBody, MENTIONWELL_WEBHOOK_SECRET)>
```

HTTP header names are case-insensitive, so reading `x-mentionwell-signature` (lowercase) is fine.

Verify it before trusting the payload.

### Node.js

```ts
import { Buffer } from "node:buffer";
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get("x-mentionwell-signature") ?? "";
  const expected = createHmac("sha256", process.env.MENTIONWELL_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex");
  const verified =
    sig.length === expected.length &&
    timingSafeEqual(Buffer.from(sig, "utf8"), Buffer.from(expected, "utf8"));
  if (!verified) return new Response("invalid signature", { status: 401 });
  const { event, siteSlug, post, paths } = JSON.parse(raw);
  // post.slug, post.title, post.publishedAt
  // event is "post.published", "post.updated", "post.revalidate", or "post.deleted"
  // if paths exists, revalidate those paths; otherwise sync the full post payload.
}
```

### Python

```python
import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

## Delivery & retries

Webhook delivery happens once during the publish flow. A delivery is considered successful if your endpoint returns a 2xx response.

If it fails (non-2xx or network error), the post still publishes inside Mentionwell and remains live on the Reader API — only the webhook fan-out is missed. The post's `deliveryStatus` flips to `failed` and the last error message is stored on the post; the dashboard surfaces these in the Articles list with a "Push failed" badge.

To retry a failed delivery, use the dashboard retry control where available or republish the post. Future versions will add automatic retries with backoff — track via the `deliveryStatus` field if you need to backfill manually.

> If your endpoint is unreliable, prefer pull mode: your destination reads the Reader API on its own ISR/SWR cycle and can never miss a publish.

## Where to find the secret

Open the [Connect-destination wizard](/docs/connect-wizard) — your webhook secret is shown on the **Setup** step with a copy button. The secret looks like `whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` (`whsec_` prefix plus 48 hex chars).

> The webhook secret is **different** from the API key. The API key (`mw_read_...`) authorises reading posts; the webhook secret (`whsec_...`) authenticates outbound pushes from Mentionwell. Both are derived per-site — they're the same value every time you view them, but they only exist for this site.


---

Canonical URL: https://mentionwell.com/docs/webhooks
Live HTML version: https://mentionwell.com/docs/webhooks
Section: API reference
Site index for AI ingestion: https://mentionwell.com/llms.txt
Full reference: https://mentionwell.com/llms-full.txt
