A secure Express.js server implementation that handles webhook requests with signature verification and response generation.
- π Secure webhook endpoint with signature verification
- π Dynamic response generation based on event type
- β‘ Express.js server with TypeScript support
- π Environment-based configuration
- π‘οΈ Error handling and logging
- Node.js (v20 or higher)
- npm
- TypeScript
Create a .env file in the root directory with the following variables:
SERVER_PORT=4000
WEBHOOK_SECRET=your_webhook_secret_here
NODE_ENV=developmentThe server exposes a POST endpoint at / that handles incoming webhook requests. Here's the implementation:
app.post("/", (req: Request, res: Response) => {
const sigHeader = req.headers["x-signature"];
const data = req.body;
const webhookSecret = process.env.WEBHOOK_SECRET;
if (!webhookSecret) {
return res.status(500).send("Webhook secret is not set");
}
const isValid = verifySignature(data, sigHeader, webhookSecret);
if (!isValid) return res.status(403).send("Invalid signature");
return res.status(200).json({
...data,
text:
data.eventType == "request"
? "Hello, how are you?"
: "I am doing well, thank you for asking.",
saveModified: data.eventType == "request" ? true : false, // Pass `saveModified` as `true` or `false` depending on whether you want to save the modified text to the chat history.
});
});The server implements HMAC SHA-256 signature verification to ensure the authenticity of incoming webhook requests:
function verifySignature(
rawBody: string,
signature: any,
secret: string
): boolean {
try {
const cleanSignature = signature?.trim();
if (!cleanSignature) return false;
const hmac = crypto.createHmac("sha256", secret);
const rawBodyStr =
typeof rawBody === "string" ? rawBody : JSON.stringify(rawBody);
hmac.update(rawBodyStr);
const expected = hmac.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(cleanSignature),
Buffer.from(expected)
);
} catch (error) {
console.error("Signature verification error:", error);
return false;
}
}The server includes professional error handling and logging during startup:
app
.listen(PORT, () => {
console.log(`π Server is running on http://localhost:${PORT}`);
console.log(`π Environment: ${process.env.NODE_ENV || "development"}`);
})
.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
console.error(`β Port ${PORT} is already in use`);
} else {
console.error("β Server failed to start:", error.message);
}
process.exit(1);
});- Signature Verification: All incoming webhook requests must include a valid signature in the
x-signatureheader - Environment Variables: Sensitive configuration is managed through environment variables
- Error Handling: Comprehensive error handling for both startup and runtime errors
- Type Safety: TypeScript implementation for better type safety and development experience
The server responds with a JSON object that includes:
- All original request data
- A dynamic text response based on the event type:
- For
requestevents: "Hello, how are you?" - For other events: "I am doing well, thank you for asking."
- For
- A
saveModifiedflag that controls chat history storage for intercepted messages:- For
requestevents:false(default) - For
responseevents:true(default) - This behavior can be overridden by explicitly setting the flag in the request
- For
Example Response:
{
"eventType": "request",
"text": "Hello, how are you?",
"saveModified": false
// ... other data
}You can override the default saveModified behavior by including it in your request:
curl -X POST http://localhost:4000 \
-H "Content-Type: application/json" \
-H "x-signature: YOUR_GENERATED_SIGNATURE" \
-d '{"eventType": "request", "saveModified": true}'The server returns appropriate HTTP status codes:
200: Successful request403: Invalid signature500: Server configuration error (missing webhook secret)
To start the development server:
npm install
npm run devFor production deployment:
npm install
npm run devTo test the webhook endpoint, you'll need to:
- Generate a valid signature using your webhook secret
- Include the signature in the
x-signatureheader - Send a POST request to the endpoint
Here's how to generate a valid signature using TypeScript:
import { createHmac } from "crypto";
// Use the same secret as in your .env file
const webhookSecret: string = "your_webhook_secret_here";
// The payload you want to send
const payload: string = JSON.stringify({ eventType: "request" });
// Generate the signature
const hmac = createHmac("sha256", webhookSecret);
hmac.update(payload);
const signature: string = hmac.digest("base64");
console.log("Generated signature:", signature);You can also use this one-liner in your terminal:
echo -n '{"eventType":"request"}' | openssl dgst -sha256 -hmac "your_webhook_secret_here" -binary | base64Example test request with a properly generated signature:
curl -X POST http://localhost:4000 \
-H "Content-Type: application/json" \
-H "x-signature: YOUR_GENERATED_SIGNATURE" \
-d '{"eventType": "request"}'To expose your local server to the internet with HTTPS, you can use ngrok. This is particularly useful for testing webhooks that require HTTPS endpoints.
- Visit ngrok downloads page to download and install ngrok for your platform
- Sign up for a free ngrok account to get your authtoken
- Configure ngrok with your authtoken:
ngrok config add-authtoken <your-authtoken>To create an HTTPS tunnel to your local server:
ngrok http 4000This will create a secure tunnel to your local server running on port 4000 and provide you with:
- A public HTTPS URL (e.g., https://your-tunnel.ngrok.io)
- A web interface at http://localhost:4040 for inspecting webhook requests
The HTTPS URL can be used as your webhook endpoint for testing with external services that require HTTPS.
Update your webhook test command using the ngrok URL:
curl -X POST https://your-tunnel.ngrok.io \
-H "Content-Type: application/json" \
-H "x-signature: YOUR_GENERATED_SIGNATURE" \
-d '{"eventType": "request"}'Note: The ngrok URL changes each time you restart ngrok unless you have a paid plan with fixed domains.