-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add webhooks #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: first-draft
Are you sure you want to change the base?
Conversation
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughRemoves 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this 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: |
There was a problem hiding this comment.
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.
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"; |
There was a problem hiding this comment.
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.
import crypto from "node:crypto"; | |
import crypto from "crypto"; |
Copilot uses AI. Check for mistakes.
@coderabbitai review |
✅ Actions performedReview triggered.
|
1 similar comment
✅ Actions performedReview triggered.
|
There was a problem hiding this 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
📒 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'?
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 }); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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" }); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
// 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) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
// 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.
There was a problem hiding this 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 🚢
Add comprehensive webhooks documentation
What's New
Key Features
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 lifecycleAddresses major documentation gap with comprehensive webhook implementation guidance.
Summary by CodeRabbit
Documentation
Chores