Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
99 changes: 97 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,101 @@ 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>

Additional 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" />
<br />
</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 +193,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 +609,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).
:::
:::
20 changes: 16 additions & 4 deletions docusaurus/static/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5974,9 +5974,21 @@ You can set webhook configurations inside the file `./config/server`.
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.
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`:
:::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:

</Tabs>

Additional external examples: <ul><li>

</Tabs>

Expand Down Expand Up @@ -9257,7 +9269,7 @@ Strapi Cloud will deploy the updated code in Strapi 5 and will automatically run
<details style={{backgroundColor: 'transparent', border: 'solid 1px #4945ff' }}>
<summary style={{fontSize: '18px'}}>How do I keep the legacy <code>attributes</code> wrapper during the migration?</summary>

- For REST clients, add the `Strapi-Response-Format: v4` header while you refactor your code. The [new response format breaking change](/cms/migration/v4-to-v5/breaking-changes/new-response-format#use-the-compatibility-header-while-migrating) shows where to add the header in `curl`, `fetch`, and Axios requests.
- For REST clients, add the `Strapi-Response-Format: v4` header while you refactor your code. The [new response format breaking change](/cms/migration/v4-to-v5/breaking-changes/new-response-format#migration) shows where to add the header in `curl`, `fetch`, and Axios requests.
- For GraphQL clients, enable `v4CompatibilityMode` and follow the steps of the [GraphQL API migration documentation](/cms/migration/v4-to-v5/breaking-changes/graphql-api-updated#migration) to gradually remove `attributes`.
- REST responses continue to expose both `id` (legacy) and [`documentId`](/cms/migration/v4-to-v5/breaking-changes/use-document-id) when the header is enabled. GraphQL never exposes numeric `id`, so update your queries to use `documentId` even before you turn compatibility mode off.

Expand Down Expand Up @@ -9364,7 +9376,7 @@ Follow the steps below and leverage retro-compatibility headers and guided migra

### Migrate REST API calls

1. Enable the compatibility header everywhere you still expect `attributes`, by adding `Strapi-Response-Format: v4` to REST calls in HTTP clients, SDKs, and middleware (see the [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/new-response-format#use-the-compatibility-header-while-migrating) for concrete examples).
1. Enable the compatibility header everywhere you still expect `attributes`, by adding `Strapi-Response-Format: v4` to REST calls in HTTP clients, SDKs, and middleware (see the [breaking change entry](/cms/migration/v4-to-v5/breaking-changes/new-response-format#migration) for concrete examples).
2. While the header is on, audit existing payloads. Capture representative responses (including populated relations, components, and media) so you can verify that legacy consumers keep working during the transition.
3. Update and test each client by:
- removing `data.attributes` access,
Expand Down