Skip to content
Merged
Changes from 14 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0008696
docs(backend): correct TypeScript code fences in TS tabs (controllers…
web-flow Nov 20, 2025
49a15fb
docs(bundlers): clarify webpack config example rename and JS/TS filen…
web-flow Nov 20, 2025
9502ba1
docs(routes): add guidance to prefer fully-qualified handler names in…
web-flow Nov 20, 2025
8ad2c1f
docs(api-tokens): add concise security tip (least privilege, rotation…
web-flow Nov 20, 2025
aff6acc
docs(controllers): add caution about validateQuery/sanitizeQuery/sani…
web-flow Nov 20, 2025
afa9417
docs(policies): clarify scoped policy folders and fix example path
web-flow Nov 20, 2025
1badc73
docs(webhooks): add signature verification tip and fix TS config path
web-flow Nov 20, 2025
fc950e2
Limit PR scope based on title; keep only intended doc(s); revert unre…
web-flow Nov 20, 2025
0248827
Webhooks docs: expand security guidance on signing and verifying payl…
web-flow Nov 20, 2025
9447650
Webhooks docs: add HMAC verification example and external references;…
web-flow Nov 20, 2025
38b7e21
Webhooks docs: wrap HMAC verification example in <details> with summa…
web-flow Nov 20, 2025
f313f58
Webhooks docs: convert HMAC verification example to JS/TS Tabs (PR #2…
web-flow Nov 20, 2025
c25851e
Webhooks docs: use ExternalLink components for external examples (PR …
web-flow Nov 20, 2025
bcae76e
Webhooks docs: use ExternalLink components for Learn more links (PR #…
web-flow Nov 20, 2025
d231db5
Apply suggestion from @pwizla
pwizla Nov 20, 2025
0511124
Apply suggestion from @pwizla
pwizla Nov 20, 2025
d10b3f6
Fix syntax
pwizla Nov 20, 2025
8428498
Update llms.txt
pwizla Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 93 additions & 2 deletions docusaurus/docs/cms/backend-customization/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,97 @@ export default {
Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information.

To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook.

:::tip Verify signatures
In addition to auth headers, sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks.

- Generate a shared secret and store it in environment variables
- Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp
- Send the signature (and timestamp) in headers (e.g., `X‑Webhook‑Signature`, `X‑Webhook‑Timestamp`)
- On receipt, recompute the HMAC and compare using a constant‑time check
- Reject if the signature is invalid or the timestamp is too old to mitigate replay

Learn more: <ExternalLink to="https://owasp.org/www-community/attacks/Replay_Attack" text="OWASP replay attacks" />, <ExternalLink to="https://nodejs.org/api/crypto.html#class-hmac" text="Node.js HMAC" />.
:::

<details>
<summary>Example: Verify HMAC signatures (Node.js)</summary>

Here is a minimal Node.js middleware example (pseudo‑code) showing HMAC verification:

<Tabs groupId="js-ts">
<TabItem value="js" label="JavaScript">

```js title="/src/middlewares/verify-webhook.js"
const crypto = require("crypto");

module.exports = (config, { strapi }) => {
const secret = process.env.WEBHOOK_SECRET;

return async (ctx, next) => {
const signature = ctx.get("X-Webhook-Signature");
const timestamp = ctx.get("X-Webhook-Timestamp");
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");

// Compute HMAC over raw body + timestamp
const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || "";
const hmac = crypto.createHmac("sha256", secret);
hmac.update(timestamp + "." + raw);
const expected = "sha256=" + hmac.digest("hex");

// Constant-time compare + basic replay protection
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
const skew = Math.abs(Date.now() - Number(timestamp));
if (!ok or skew > 5 * 60 * 1000) {
return ctx.unauthorized("Invalid or expired signature");
}

await next();
};
};
```

</TabItem>

<TabItem value="ts" label="TypeScript">

```ts title="/src/middlewares/verify-webhook.ts"
import crypto from "node:crypto"

export default (config: unknown, { strapi }: any) => {
const secret = process.env.WEBHOOK_SECRET as string;

return async (ctx: any, next: any) => {
const signature = ctx.get("X-Webhook-Signature") as string;
const timestamp = ctx.get("X-Webhook-Timestamp") as string;
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");

// Compute HMAC over raw body + timestamp
const raw: string = ctx.request.rawBody || (ctx.request.body && JSON.stringify(ctx.request.body)) || "";
const hmac = crypto.createHmac("sha256", secret);
hmac.update(`${timestamp}.${raw}`);
const expected = `sha256=${hmac.digest("hex")}`;

// Constant-time compare + basic replay protection
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
const skew = Math.abs(Date.now() - Number(timestamp));
if (!ok || skew > 5 * 60 * 1000) {
return ctx.unauthorized("Invalid or expired signature");
}

await next();
};
};
```

</TabItem>
</Tabs>

External examples: <ExternalLink to="https://docs.github.com/webhooks/using-webhooks/validating-webhook-deliveries" text="GitHub — Validating webhook deliveries" />, <ExternalLink to="https://stripe.com/docs/webhooks/signatures" text="Stripe — Verify webhook signatures" />.

</details>


Another way is to define `defaultHeaders` to add to every webhook request.

You can configure these global headers by updating the file at `./config/server`:
Expand All @@ -98,7 +189,7 @@ module.exports = {

<TabItem value="ts" label="TypeScript">

```js title="./config.server.ts"
```js title="./config/server.ts"
export default {
webhooks: {
defaultHeaders: {
Expand Down Expand Up @@ -514,4 +605,4 @@ The event is triggered when a [release](/cms/features/releases) is published.

:::tip
If you want to learn more about how to use webhooks with Next.js, please have a look at the [dedicated blog article](https://strapi.io/blog/how-to-create-an-ssg-static-site-generation-application-with-strapi-webhooks-and-nextjs).
:::
:::