Skip to content

Conversation

MantisClone
Copy link
Member

@MantisClone MantisClone commented Oct 4, 2025

Add comprehensive webhooks documentation

What's New

  • New webhook docs: Complete implementation guide with event types, security, and code examples
  • API Reference: Detailed webhooks page with payload examples and Express.js/Next.js implementations
  • API Features: High-level webhooks overview with use cases and setup guidance
  • Request docs restructure: Split create/query requests into focused pages

Key Features

  • 12+ webhook event types with realistic payload examples
  • HMAC SHA-256 security implementation with working code
  • 3-retry system (1s, 5s, 15s delays) with idempotency support
  • Portal integration for testing and configuration
  • Production-ready Express.js and Next.js examples

Documentation Structure

  • /api-features/webhooks-events - High-level concepts and workflow
  • /api-reference/webhooks - Complete technical implementation guide
  • /api-features/create-requests - Request creation workflows
  • /api-features/query-requests - Status monitoring and lifecycle

Addresses major documentation gap with comprehensive webhook implementation guidance.

Summary by CodeRabbit

  • Documentation

    • Added dedicated pages: Create Requests and Query Requests with workflows, properties, and use cases.
    • Overhauled Webhooks & Events docs: clearer categories, real-time monitoring, advanced filtering, and implementation guidance.
    • Revamped Webhooks reference: standardized event catalog and payloads, signature verification guidance, unified Express/Next examples, headers and retry behavior clarified.
    • Removed deprecated combined page for creating/queried requests.
  • Chores

    • Updated documentation navigation: renamed “Payment Types” to “Payments,” replaced legacy Requests page, and added Query Requests under Reconciliation.

Copy link

coderabbitai bot commented Oct 4, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Removes the combined “create-requests-query-status” doc. Adds separate docs for creating requests and querying requests. Overhauls Webhooks feature doc and fully restructures the Webhooks reference with signature verification, headers, retries, and unified examples. Updates docs.json navigation to reflect new pages and renamed groups.

Changes

Cohort / File(s) Summary
Requests docs restructure
api-features/create-requests.mdx, api-features/query-requests.mdx, api-features/create-requests-query-status.mdx
Adds new Create Requests and Query Requests docs with workflows, properties, and methods; deletes the previous combined page covering creation and status querying.
Webhooks docs overhaul
api-features/webhooks-events.mdx, api-reference/webhooks.mdx
Rewrites Webhooks feature and reference: new event categories, security-focused signed delivery, explicit headers, retry behavior, and unified Express/Next handler examples with signature verification.
Navigation updates
docs.json
Renames “Payment Types” to “Payments”; replaces Requests page link to new Create Requests doc; adds Query Requests under Reconciliation.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App as Client App
  participant WH as Webhook Endpoint
  participant RN as Request Network
  note over RN,WH: Webhook Delivery with Signature Verification and Retries

  RN->>WH: POST /webhook (body, headers: x-request-network-signature, delivery-id, retry-count, test)
  WH->>WH: Verify HMAC SHA-256 signature
  alt Signature valid
    WH-->>RN: 200 OK
    WH->>App: Process event (switch by event type)
  else Signature invalid or processing error
    WH-->>RN: 4xx/5xx
    note over RN: Queue retry (up to 3 attempts, with timeout)
    RN->>WH: Retry POST (increment retry-count)
    opt On success
      WH-->>RN: 200 OK
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The author’s title “feat: add webhooks” clearly identifies the main change as adding webhook support and documentation and is concise and directly related to the primary content of the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch webhooks

Comment @coderabbitai help to get the list of available commands and usage tips.

@MantisClone MantisClone changed the title feat: add webhooks documentation feat: add webhooks Oct 4, 2025
@MantisClone MantisClone self-assigned this Oct 4, 2025
@MantisClone MantisClone marked this pull request as ready for review October 4, 2025 03:07
@Copilot Copilot AI review requested due to automatic review settings October 4, 2025 03:07
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces comprehensive webhooks documentation and restructures request-related pages to improve organization. The main goal is to address the documentation gap around webhook implementation with detailed technical guidance, security practices, and working code examples.

Key changes:

  • Complete webhooks API reference with event types, security implementation, and retry logic
  • New high-level webhooks concepts page with workflow overview
  • Split existing request documentation into focused create/query pages

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
docs.json Updates navigation structure to reorganize request documentation and add new query-requests page
api-reference/webhooks.mdx Complete rewrite with comprehensive webhook implementation guide, security, and examples
api-features/webhooks-events.mdx Updates to align with new comprehensive webhook documentation
api-features/query-requests.mdx New focused page on request status monitoring and lifecycle management
api-features/create-requests.mdx New focused page on request creation workflows and configuration
api-features/create-requests-query-status.mdx Removed as functionality split into separate create/query pages
Comments suppressed due to low confidence (1)

docs.json:1

  • [nitpick] The group name change from 'Payment Types' to 'Payments' may be too generic. Consider 'Payment Methods' or 'Payment Options' to maintain clarity about the content focus.
{

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

5. Enable/disable or delete webhooks as needed

### Local Development
Use [ngrok](https://ngrok.com/docs/traffic-policy/getting-started/agent-endpoints/cli) to receive webhooks locally:
Copy link
Preview

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ngrok documentation link appears to be incorrect or overly specific. Consider using the main ngrok documentation URL (https://ngrok.com/docs) or verify the current link structure.

Suggested change
Use [ngrok](https://ngrok.com/docs/traffic-policy/getting-started/agent-endpoints/cli) to receive webhooks locally:
Use [ngrok](https://ngrok.com/docs) to receive webhooks locally:

Copilot uses AI. Check for mistakes.

- Timeout limit: 30 seconds
- Must handle duplicate events (idempotency)
```javascript
import crypto from "node:crypto";
Copy link
Preview

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'node:crypto' import syntax requires Node.js 14.18.0+. Consider documenting the minimum Node.js version requirement or providing alternative import syntax for older versions.

Suggested change
import crypto from "node:crypto";
import crypto from "crypto";

Copilot uses AI. Check for mistakes.

@MantisClone
Copy link
Member Author

MantisClone commented Oct 4, 2025

@coderabbitai review

Copy link

coderabbitai bot commented Oct 4, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
Copy link

coderabbitai bot commented Oct 4, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a96598a and addbbe7.

📒 Files selected for processing (6)
  • api-features/create-requests-query-status.mdx (0 hunks)
  • api-features/create-requests.mdx (1 hunks)
  • api-features/query-requests.mdx (1 hunks)
  • api-features/webhooks-events.mdx (1 hunks)
  • api-reference/webhooks.mdx (1 hunks)
  • docs.json (2 hunks)
💤 Files with no reviewable changes (1)
  • api-features/create-requests-query-status.mdx
🧰 Additional context used
🪛 GitHub Check: Mintlify Validation (requestnetwork) - vale-spellcheck
api-features/create-requests.mdx

[warning] 37-37: api-features/create-requests.mdx#L37
Did you really mean 'Blockchain'?


[warning] 51-51: api-features/create-requests.mdx#L51
Did you really mean 'crypto'?


[warning] 66-66: api-features/create-requests.mdx#L66
Did you really mean 'Mainnet'?


[warning] 66-66: api-features/create-requests.mdx#L66
Did you really mean 'Ethereum'?


[warning] 66-66: api-features/create-requests.mdx#L66
Did you really mean 'Arbitrum'?


[warning] 67-67: api-features/create-requests.mdx#L67
Did you really mean 'Sidechains'?


[warning] 68-68: api-features/create-requests.mdx#L68
Did you really mean 'Testnets'?


[warning] 68-68: api-features/create-requests.mdx#L68
Did you really mean 'Sepolia'?

Comment on lines +68 to +87
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");

```javascript
app.post('/webhooks/request-network', (req, res) => {
try {
// Process webhook event
const { eventType, data } = req.body;

// Your business logic here
processEvent(eventType, data);

// Return success status
res.status(200).send('OK');

} catch (error) {
// Return error status for retry
console.error('Webhook processing error:', error);
res.status(500).send('Error processing webhook');
}
});
```
</Tab>
return signature === expectedSignature;
}

// Usage in your webhook handler
app.post("/webhook", (req, res) => {
const signature = req.headers["x-request-network-signature"];

<Tab title="Security">
**Webhook Security:**
- Verify webhook signatures
- Use HTTPS only
- Implement request validation
- Rate limit webhook endpoints
```javascript
const crypto = require('crypto');

function verifyWebhookSignature(req) {
const signature = req.headers['x-request-signature'];
const timestamp = req.headers['x-request-timestamp'];
const payload = JSON.stringify(req.body);

// Verify timestamp (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) { // 5 minute tolerance
return false;
}

// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(timestamp + payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
```
</Tab>
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}

<Tab title="Retry Logic">
**Automatic Retries:**
- Retry schedule: 1s, 5s, 25s, 125s, 625s
- Maximum 5 retry attempts
- Exponential backoff with jitter
- Dead letter queue for failed webhooks
**Error Handling:**
```javascript
app.post('/webhooks/request-network', async (req, res) => {
try {
const { eventType, data } = req.body;

// Process event
await processEvent(eventType, data);

res.status(200).send('OK');

} catch (error) {
if (error.permanent) {
// Don't retry permanent errors
res.status(400).send('Permanent error');
} else {
// Retry temporary errors
res.status(500).send('Temporary error');
}
}
});
```
</Tab>
</Tabs>
// Process webhook...
res.status(200).json({ success: true });
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix signature verification to use the raw request payload.

HMAC must be computed on the exact byte stream sent by Request Network. Re-stringifying the parsed object (JSON.stringify(payload)) mutates field ordering/whitespace and will make every signature check fail in production. Accept the raw body (Buffer/string) from your server framework and feed that directly into crypto.createHmac().update(...).

-import crypto from "node:crypto";
-
-function verifyWebhookSignature(payload, signature, secret) {
-  const expectedSignature = crypto
-    .createHmac("sha256", secret)
-    .update(JSON.stringify(payload))
-    .digest("hex");
-  
-  return signature === expectedSignature;
-}
+import crypto from "node:crypto";
+
+function verifyWebhookSignature(rawBody, signature, secret) {
+  const expectedSignature = crypto
+    .createHmac("sha256", secret)
+    .update(rawBody)
+    .digest("hex");
+
+  return signature === expectedSignature;
+}

Update the surrounding prose to call out that rawBody is the unparsed request payload captured before any JSON parsing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");
```javascript
app.post('/webhooks/request-network', (req, res) => {
try {
// Process webhook event
const { eventType, data } = req.body;
// Your business logic here
processEvent(eventType, data);
// Return success status
res.status(200).send('OK');
} catch (error) {
// Return error status for retry
console.error('Webhook processing error:', error);
res.status(500).send('Error processing webhook');
}
});
```
</Tab>
return signature === expectedSignature;
}
// Usage in your webhook handler
app.post("/webhook", (req, res) => {
const signature = req.headers["x-request-network-signature"];
<Tab title="Security">
**Webhook Security:**
- Verify webhook signatures
- Use HTTPS only
- Implement request validation
- Rate limit webhook endpoints
```javascript
const crypto = require('crypto');
function verifyWebhookSignature(req) {
const signature = req.headers['x-request-signature'];
const timestamp = req.headers['x-request-timestamp'];
const payload = JSON.stringify(req.body);
// Verify timestamp (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) { // 5 minute tolerance
return false;
}
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(timestamp + payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
```
</Tab>
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
<Tab title="Retry Logic">
**Automatic Retries:**
- Retry schedule: 1s, 5s, 25s, 125s, 625s
- Maximum 5 retry attempts
- Exponential backoff with jitter
- Dead letter queue for failed webhooks
**Error Handling:**
```javascript
app.post('/webhooks/request-network', async (req, res) => {
try {
const { eventType, data } = req.body;
// Process event
await processEvent(eventType, data);
res.status(200).send('OK');
} catch (error) {
if (error.permanent) {
// Don't retry permanent errors
res.status(400).send('Permanent error');
} else {
// Retry temporary errors
res.status(500).send('Temporary error');
}
}
});
```
</Tab>
</Tabs>
// Process webhook...
res.status(200).json({ success: true });
});
import crypto from "node:crypto";
function verifyWebhookSignature(rawBody, signature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return signature === expectedSignature;
}
// Usage in your webhook handler
app.post("/webhook", (req, res) => {
const signature = req.headers["x-request-network-signature"];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
// Process webhook...
res.status(200).json({ success: true });
});
🤖 Prompt for AI Agents
In api-reference/webhooks.mdx around lines 68 to 87, the example computes the
HMAC from a JSON.stringified parsed payload which will change byte
ordering/whitespace and break verification; change the example and surrounding
prose to use the raw unparsed request body (Buffer or string) captured before
any JSON parsing and pass that rawBody directly into
crypto.createHmac().update(...). Also update the usage text to explicitly state
that rawBody is the exact unparsed request payload captured by your framework
(e.g., via a raw body middleware or request event) and must be used instead of
req.body when verifying signatures.

Comment on lines +253 to 297
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

### E-commerce Order Processing
app.use(express.json());

<CodeGroup>
```javascript Express.js Handler
app.post('/webhooks/payment-confirmed', async (req, res) => {
app.post("/webhook/payment", async (req, res) => {
try {
// Verify webhook signature
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Unauthorized');
// Verify signature
const signature = req.headers["x-request-network-signature"];
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");

if (signature !== expectedSignature) {
return res.status(401).json({ error: "Invalid signature" });
}

// Check for test webhook
const isTest = req.headers["x-request-network-test"] === "true";

const { eventType, data } = req.body;
// Process webhook based on event type
const { event, requestId } = req.body;

if (eventType === 'payment_confirmed') {
const orderId = data.metadata.orderId;

// Update order status
await updateOrder(orderId, {
status: 'paid',
paymentHash: data.transactionHash,
paidAt: data.timestamp,
paymentMethod: 'crypto'
});

// Send confirmation email
await sendOrderConfirmation(orderId);

// Trigger fulfillment
await triggerOrderFulfillment(orderId);

console.log(`Order ${orderId} marked as paid`);
switch (event) {
case "payment.confirmed":
await handlePaymentConfirmed(req.body);
break;
case "payment.processing":
await handlePaymentProcessing(req.body);
break;
case "compliance.updated":
await handleComplianceUpdate(req.body);
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.status(200).send('OK');

return res.status(200).json({ success: true });

} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Error processing webhook');
console.error("Webhook processing error:", error);
return res.status(500).json({ error: "Processing failed" });
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a raw-body middleware before verifying webhook signatures.

express.json() consumes the body and re-serializes it, so JSON.stringify(req.body) will not match the signed payload. Instead, capture the raw buffer and pass it to createHmac. Without this, every production webhook verification breaks.

-import express from "express";
-import crypto from "node:crypto";
-
-const app = express();
-const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
-
-app.use(express.json());
-
-app.post("/webhook/payment", async (req, res) => {
+import express from "express";
+import crypto from "node:crypto";
+
+const app = express();
+const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
+
+app.use(
+  express.raw({
+    type: "application/json",
+    verify: (req, _res, buf) => {
+      req.rawBody = buf;
+    },
+  })
+);
+
+app.post("/webhook/payment", async (req, res) => {
   try {
-    // Verify signature
-    const signature = req.headers["x-request-network-signature"];
-    const expectedSignature = crypto
-      .createHmac("sha256", WEBHOOK_SECRET)
-      .update(JSON.stringify(req.body))
-      .digest("hex");
-
-    if (signature !== expectedSignature) {
+    const signature = req.headers["x-request-network-signature"];
+    const rawBody = req.rawBody;
+
+    const expectedSignature = crypto
+      .createHmac("sha256", WEBHOOK_SECRET)
+      .update(rawBody)
+      .digest("hex");
+
+    if (!signature || signature !== expectedSignature) {
       return res.status(401).json({ error: "Invalid signature" });
     }
 
-    // Check for test webhook
-    const isTest = req.headers["x-request-network-test"] === "true";
+    const body = JSON.parse(rawBody.toString("utf8"));
+    const isTest = req.headers["x-request-network-test"] === "true";
 
-    // Process webhook based on event type
-    const { event, requestId } = req.body;
+    const { event, requestId } = body;

Make sure the narrative explains the switch to express.raw and parsing manually after verification.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
### E-commerce Order Processing
app.use(express.json());
<CodeGroup>
```javascript Express.js Handler
app.post('/webhooks/payment-confirmed', async (req, res) => {
app.post("/webhook/payment", async (req, res) => {
try {
// Verify webhook signature
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Unauthorized');
// Verify signature
const signature = req.headers["x-request-network-signature"];
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(JSON.stringify(req.body))
.digest("hex");
if (signature !== expectedSignature) {
return res.status(401).json({ error: "Invalid signature" });
}
// Check for test webhook
const isTest = req.headers["x-request-network-test"] === "true";
const { eventType, data } = req.body;
// Process webhook based on event type
const { event, requestId } = req.body;
if (eventType === 'payment_confirmed') {
const orderId = data.metadata.orderId;
// Update order status
await updateOrder(orderId, {
status: 'paid',
paymentHash: data.transactionHash,
paidAt: data.timestamp,
paymentMethod: 'crypto'
});
// Send confirmation email
await sendOrderConfirmation(orderId);
// Trigger fulfillment
await triggerOrderFulfillment(orderId);
console.log(`Order ${orderId} marked as paid`);
switch (event) {
case "payment.confirmed":
await handlePaymentConfirmed(req.body);
break;
case "payment.processing":
await handlePaymentProcessing(req.body);
break;
case "compliance.updated":
await handleComplianceUpdate(req.body);
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.status(200).send('OK');
return res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Error processing webhook');
console.error("Webhook processing error:", error);
return res.status(500).json({ error: "Processing failed" });
}
});
import express from "express";
import crypto from "node:crypto";
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Use raw-body middleware so we get the exact bytes for HMAC verification
app.use(
express.raw({
type: "application/json",
verify: (req, _res, buf) => {
req.rawBody = buf;
},
})
);
app.post("/webhook/payment", async (req, res) => {
try {
// Verify signature against the raw payload
const signature = req.headers["x-request-network-signature"];
const rawBody = req.rawBody;
const expectedSignature = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
if (!signature || signature !== expectedSignature) {
return res.status(401).json({ error: "Invalid signature" });
}
// Parse the JSON only after verifying the signature
const body = JSON.parse(rawBody.toString("utf8"));
const isTest = req.headers["x-request-network-test"] === "true";
// Process webhook based on event type
const { event, requestId } = body;
switch (event) {
case "payment.confirmed":
await handlePaymentConfirmed(body);
break;
case "payment.processing":
await handlePaymentProcessing(body);
break;
case "compliance.updated":
await handleComplianceUpdate(body);
break;
default:
console.log(`Unhandled event: ${event}`);
}
return res.status(200).json({ success: true });
} catch (error) {
console.error("Webhook processing error:", error);
return res.status(500).json({ error: "Processing failed" });
}
});
🤖 Prompt for AI Agents
In api-reference/webhooks.mdx around lines 253 to 297, the webhook verification
uses JSON-parsed req.body (express.json()) and computes HMAC over
JSON.stringify(req.body), which will not match the original signed payload;
change the route to use express.raw({ type: "application/json" }) (or a raw-body
middleware) for the webhook endpoint to capture the raw request buffer, compute
the HMAC over that raw buffer using WEBHOOK_SECRET, then only after signature
verification parse the buffer to JSON (e.g., JSON.parse(raw.toString())) and
proceed with event handling; ensure header access and error handling remain the
same and preserve test-header logic.

Comment on lines +303 to +342
// app/api/webhook/route.ts
import crypto from "node:crypto";
import { NextResponse } from "next/server";

```javascript SaaS Subscription Handler
app.post('/webhooks/subscription-events', async (req, res) => {
export async function POST(request: Request) {
try {
const { eventType, data } = req.body;
const body = await request.json();

switch (eventType) {
case 'subscription_renewed':
// Extend subscription period
await extendSubscription(data.customerId, data.renewalPeriod);

// Send renewal confirmation
await sendRenewalConfirmation(data.customerId);
break;

case 'subscription_failed':
// Handle failed payment
await handleFailedSubscriptionPayment(data.customerId);

// Send payment failure notification
await sendPaymentFailureNotification(data.customerId);
break;

case 'subscription_cancelled':
// Deactivate subscription
await deactivateSubscription(data.customerId);
break;
// Verify signature
const signature = request.headers.get("x-request-network-signature");
const expectedSignature = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(body))
.digest("hex");

if (signature !== expectedSignature) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}

// Process webhook
const { event, requestId } = body;

res.status(200).send('OK');
// Your business logic here
await processWebhookEvent(event, body);

return NextResponse.json({ success: true }, { status: 200 });

} catch (error) {
console.error('Subscription webhook error:', error);
res.status(500).send('Error processing subscription webhook');
console.error("Webhook error:", error);

if (error instanceof ResourceNotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}

return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
});
```
</CodeGroup>
### Database Integration
<CodeGroup>
```sql Payment Tracking
-- Create table for payment tracking
CREATE TABLE payment_events (
id SERIAL PRIMARY KEY,
request_id VARCHAR(255) NOT NULL,
event_type VARCHAR(50) NOT NULL,
transaction_hash VARCHAR(66),
block_number INTEGER,
amount DECIMAL(36, 18),
currency VARCHAR(20),
payer_address VARCHAR(42),
payee_address VARCHAR(42),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP,
INDEX idx_request_id (request_id),
INDEX idx_event_type (event_type),
INDEX idx_created_at (created_at)
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Read and verify the raw body in the Next.js example.

request.json() destroys the original payload and JSON.stringify(body) alters ordering, causing signature mismatches. Use await request.text() to get the raw string, verify the signature against it, then JSON.parse for processing.

-export async function POST(request: Request) {
+export async function POST(request: Request) {
   try {
-    const body = await request.json();
-
-    // Verify signature
-    const signature = request.headers.get("x-request-network-signature");
-    const expectedSignature = crypto
-      .createHmac("sha256", process.env.WEBHOOK_SECRET!)
-      .update(JSON.stringify(body))
-      .digest("hex");
-
-    if (signature !== expectedSignature) {
+    const rawBody = await request.text();
+    const signature = request.headers.get("x-request-network-signature");
+    const expectedSignature = crypto
+      .createHmac("sha256", process.env.WEBHOOK_SECRET!)
+      .update(rawBody)
+      .digest("hex");
+
+    if (!signature || signature !== expectedSignature) {
       return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
     }
 
+    const body = JSON.parse(rawBody);
     // Process webhook
     const { event, requestId } = body;

Also note in the surrounding text that the App Router requires disabling automatic body parsing if using middleware.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// app/api/webhook/route.ts
import crypto from "node:crypto";
import { NextResponse } from "next/server";
```javascript SaaS Subscription Handler
app.post('/webhooks/subscription-events', async (req, res) => {
export async function POST(request: Request) {
try {
const { eventType, data } = req.body;
const body = await request.json();
switch (eventType) {
case 'subscription_renewed':
// Extend subscription period
await extendSubscription(data.customerId, data.renewalPeriod);
// Send renewal confirmation
await sendRenewalConfirmation(data.customerId);
break;
case 'subscription_failed':
// Handle failed payment
await handleFailedSubscriptionPayment(data.customerId);
// Send payment failure notification
await sendPaymentFailureNotification(data.customerId);
break;
case 'subscription_cancelled':
// Deactivate subscription
await deactivateSubscription(data.customerId);
break;
// Verify signature
const signature = request.headers.get("x-request-network-signature");
const expectedSignature = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(JSON.stringify(body))
.digest("hex");
if (signature !== expectedSignature) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// Process webhook
const { event, requestId } = body;
res.status(200).send('OK');
// Your business logic here
await processWebhookEvent(event, body);
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error('Subscription webhook error:', error);
res.status(500).send('Error processing subscription webhook');
console.error("Webhook error:", error);
if (error instanceof ResourceNotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
});
```
</CodeGroup>
### Database Integration
<CodeGroup>
```sql Payment Tracking
-- Create table for payment tracking
CREATE TABLE payment_events (
id SERIAL PRIMARY KEY,
request_id VARCHAR(255) NOT NULL,
event_type VARCHAR(50) NOT NULL,
transaction_hash VARCHAR(66),
block_number INTEGER,
amount DECIMAL(36, 18),
currency VARCHAR(20),
payer_address VARCHAR(42),
payee_address VARCHAR(42),
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP,
INDEX idx_request_id (request_id),
INDEX idx_event_type (event_type),
INDEX idx_created_at (created_at)
);
}
// app/api/webhook/route.ts
import crypto from "node:crypto";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
try {
// Read raw body for signature verification
const rawBody = await request.text();
const signature = request.headers.get("x-request-network-signature");
const expectedSignature = crypto
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex");
if (!signature || signature !== expectedSignature) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// Parse JSON after verifying signature
const body = JSON.parse(rawBody);
// Process webhook
const { event, requestId } = body;
// Your business logic here
await processWebhookEvent(event, body);
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Webhook error:", error);
if (error instanceof ResourceNotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 });
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
🤖 Prompt for AI Agents
In api-reference/webhooks.mdx around lines 303 to 342, the handler uses
request.json() and JSON.stringify(body) which breaks signature verification;
change to await request.text() to obtain the raw request body, compute the HMAC
against that raw string to verify the x-request-network-signature, then parse
the raw string with JSON.parse(...) for downstream processing; also add the App
Router note to disable automatic body parsing for this route (so the raw body is
available) by setting the route's config to turn off body parsing or using the
recommended Next.js middleware setting.

Copy link

@bassgeta bassgeta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mintlify really dislikes our vocabulary, a ton of "Did you really mean ?" :D
Other than that, it seems legit, really concise and straight to the point 👌
There is the one comment left by CodeRabbit regarding stringifying the body before checking it which we should double check, but other than that it's looking stellar 🚢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Docs - Clarify webhook payloads, signatures, and ngrok for receiving webhooks in a development app
2 participants