Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
487 changes: 250 additions & 237 deletions docs/developer-docs/6.x/navigation.tsx

Large diffs are not rendered by default.

40 changes: 40 additions & 0 deletions docs/developer-docs/6.x/reference/sdk/webhooks.ai.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
AI Context: Webhook Verification (webhook-verification.mdx)

Source of Information:
1. Webiny SDK source: packages/sdk/src/methods/webhooks/verifyWebhookPayload.ts
2. Webiny API core abstractions: packages/api-core/src/features/webhooks/WebhookVerifyPayload/abstractions.ts
3. Standard Webhooks specification: https://www.standardwebhooks.com
4. standardwebhooks npm package (used internally by the SDK)

Key Documentation Decisions:
1. Focused exclusively on the SDK's createVerifyWebhookPayload function — no DI container or feature registration
2. Did not cover setting up a receiver endpoint (Express, Next.js, etc.) — the article is about the verification API itself
3. Did not explain the Result pattern inline — linked to existing core-concepts/basic/result docs
4. Did not cover the signing side (how Webiny signs outgoing payloads) — that is internal
5. Mentioned the Node.js-only constraint prominently since the import path looks like it could be used anywhere
6. Did not cover webhook configuration in the Admin UI — the article assumes webhooks are already set up
7. Used process.env.WEBHOOK_SECRET as the idiomatic way to pass the secret — the SDK accepts any string

Understanding of Webhook Verification:
- Webiny follows the Standard Webhooks spec for signing and verifying payloads
- Three headers are sent with every delivery: webhook-id, webhook-timestamp, webhook-signature
- The SDK function createVerifyWebhookPayload is a factory — call it once with the secret, then reuse the returned function per request
- If the secret is undefined, the factory returns a function that throws immediately (fail-fast for misconfigured environments)
- Verification uses HMAC-SHA256 under the hood (via standardwebhooks library)
- The standardwebhooks library enforces a timestamp tolerance window to prevent replay attacks
- The raw body must be passed — parsing to JSON before verification breaks the signature

Related Documents:
- core-concepts/basic/result.mdx — the Result pattern used by the verification return type
- headless-cms/event-handlers.mdx — shows webhook calls from the Webiny side (outgoing, not incoming verification)

Key Code Locations:
- /webiny-js-v6/packages/sdk/src/methods/webhooks/verifyWebhookPayload.ts — the SDK implementation
- /webiny-js-v6/packages/api-core/src/features/webhooks/WebhookVerifyPayload/abstractions.ts — the interface and header types
- /webiny-js-v6/packages/webhooks/src/api/features/WebhookVerifyPayload/WebhookVerifyPayload.ts — the internal API implementation (not SDK)

Tone Guidelines:
- Direct and practical — the reader wants to verify webhooks, not learn cryptography
- Explain "why verify" briefly, then focus on "how to verify"
- Keep code examples minimal and copy-paste ready
- Trust the reader to know Result pattern basics — just link to the docs
84 changes: 84 additions & 0 deletions docs/developer-docs/6.x/reference/sdk/webhooks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
id: sbwmyo92
title: Webhooks
description: Verify incoming Webiny webhook payloads using the Webiny SDK to ensure authenticity and integrity.
---

import { Alert } from "@/components/Alert";

<Alert type="success" title="WHAT YOU'LL LEARN">

- Why you should verify webhook payloads before processing them
- Which headers Webiny sends with every webhook delivery
- How to use `createVerifyWebhookPayload` from `@webiny/sdk` to verify a payload
- How to handle verification failures

</Alert>

## Overview

When Webiny delivers a webhook, it signs the payload so that your receiving service can confirm two things: the request genuinely came from Webiny, and the payload was not tampered with in transit.

Webiny follows the [Standard Webhooks](https://www.standardwebhooks.com) specification. Every delivery includes a set of headers that carry a message ID, a timestamp, and a cryptographic signature. The `@webiny/sdk` package provides a `createVerifyWebhookPayload` function that checks these headers against the raw request body and your signing secret.

<Alert type="warning" title="Node.js only">

`@webiny/sdk/webhooks` relies on the `standardwebhooks` library, which is not compatible with browser environments. Only use it in server-side code.

</Alert>

## Webhook Headers

Webiny sends three headers with every webhook delivery:

| Header | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `webhook-id` | A unique identifier for the message. You can use this to detect duplicate deliveries. |
| `webhook-timestamp` | A Unix timestamp (in seconds) of when the message was sent. |
| `webhook-signature` | A Base64-encoded signature computed from the message ID, timestamp, and body using your signing secret. |

Your receiving endpoint must forward all three headers — along with the **raw request body** — to the verification function. Parsing the body as JSON before verification will invalidate the signature.

## Verifying a Payload

Start by creating a verification function with your signing secret:

```typescript
import { createVerifyWebhookPayload } from "@webiny/sdk/webhooks";

const verifyWebhookPayload = createVerifyWebhookPayload(process.env.WEBHOOK_SECRET);
```

`createVerifyWebhookPayload` accepts the signing secret and returns an async function you call for each incoming request. If the secret is `undefined`, the returned function throws immediately — this lets you catch misconfigured environments early.

Call the returned function with the raw body and the three webhook headers:

```typescript
const result = await verifyWebhookPayload(rawBody, {
"webhook-id": headers["webhook-id"],
"webhook-timestamp": headers["webhook-timestamp"],
"webhook-signature": headers["webhook-signature"]
});
```

The function returns a [`Result`](/docs/developer-docs/6.x/core-concepts/basic/result) — check it before processing the payload:

```typescript
if (result.isFail()) {
// Signature is invalid — reject the request.
console.error("Webhook verification failed:", result.error);
return;
}

const payload = result.value;
```

## Error Handling

Verification fails when:

- **The signature does not match** — the payload was tampered with or the wrong secret was used.
- **The timestamp is too old** — the `standardwebhooks` library rejects messages outside a tolerance window to prevent replay attacks.
- **A required header is missing** — one or more of the three webhook headers was not included in the request.

When verification fails, `result.isFail()` returns `true` and `result.error` contains the underlying error with a message describing the reason. Do not process the payload — return an appropriate HTTP error status to the caller.
3 changes: 2 additions & 1 deletion generateDocs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ process.env.NODE_PATH = process.cwd();
const tsNode = require("ts-node");
const { resolve } = require("path");
const yargs = require("yargs");
const { hideBin } = require("yargs/helpers");
const rimraf = require("rimraf");

tsNode.register({
Expand All @@ -13,7 +14,7 @@ const { App, AppConfig } = require("@webiny/docs-generator");
const { default: docsConfig } = require("./docs.config");

(async () => {
const { watch, watchOnly, version } = yargs()
const { watch, watchOnly, version } = yargs(hideBin(process.argv))
.version(false)
.option("v", {
alias: "version",
Expand Down
4 changes: 1 addition & 3 deletions generator/src/app/CompositeDocumentRootWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export class CompositeDocumentRootWatcher implements IDocumentRootWatcher {
}

async watch(onFile: OnFile, onEvent?: OnWatchEvent): Promise<void> {
this.documentRootWatchers.forEach(documentRootWatcher =>
documentRootWatcher.watch(onFile, onEvent)
);
await Promise.all(this.documentRootWatchers.map(w => w.watch(onFile, onEvent)));
}
}
5 changes: 3 additions & 2 deletions generator/src/app/DocumentRootWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export class DocumentRootWatcher implements IDocumentRootWatcher {
.map(file => file.getSourcePath())
.filter((value?: string): value is string => !!value)
.map(path => this.enforceExtensionOrGlob(path));

const watcher = watch([...paths, ...this.additionalFiles]);

watcher
Expand All @@ -38,6 +37,8 @@ export class DocumentRootWatcher implements IDocumentRootWatcher {
}

private enforceExtensionOrGlob(value: string) {
return [".js", ".ts", ".tsx"].some(ext => value.endsWith(ext)) ? value : `${value}*`;
return [".js", ".ts", ".tsx", ".mdx"].some(ext => value.endsWith(ext))
? value
: `${value}*`;
}
}