Verifying webhook signatures ensures that incoming webhooks are authentic and sent by Coinbase, protecting your application from malicious requests and replay attacks.
When you create a webhook subscription, the response includes a metadata.secret that serves as your signing key.Each webhook request includes an X-Hook0-Signature header that looks like this:
Now integrate the verification function into your webhook endpoint. This example shows:
How to configure Express to preserve the raw request body (required for signature verification)
How to extract the signature header and webhook secret
How to call the verification function before processing the webhook
How to handle both valid and invalid webhooks appropriately
Important: You must use express.raw() middleware instead of express.json() to preserve the raw request body. The signature is computed against the raw bytes, so parsing the JSON first will break verification.
webhook-endpoint.js
Copy
Ask AI
const express = require("express");const app = express();// Important: Get raw body for signature verificationapp.use(express.raw({ type: "application/json" }));app.post("/webhook", (req, res) => { // Step 1: Extract the raw payload (must be string for signature verification) const payload = req.body.toString(); // Step 2: Get the signature from the X-Hook0-Signature header const signature = req.headers["x-hook0-signature"]; // Step 3: Get your webhook secret (from metadata.secret in subscription creation) const secret = process.env.WEBHOOK_SECRET; // Step 4: Verify the webhook signature if (verifyWebhookSignature(payload, signature, secret, req.headers)) { console.log("✅ Authentic webhook"); // Step 5: Parse the JSON payload (only after verification!) const event = JSON.parse(payload); // Step 6: Process your webhook event console.log("Transaction detected:", event.data.transactionHash); // Add your business logic here... // Step 7: Return 200 to acknowledge receipt res.status(200).send("OK"); } else { console.log("❌ Invalid webhook - rejected"); res.status(400).send("Invalid signature"); }});
Never hardcode webhook secrets in your code. Use environment variables or a secure secrets manager:
Copy
Ask AI
// ✅ Good - using environment variablesconst secret = process.env.WEBHOOK_SECRET;// ❌ Bad - hardcoded secretconst secret = "whsec_abc123...";
Use HTTPS only
Always use HTTPS endpoints for your webhooks. HTTP endpoints expose your webhook data to interception and tampering.
Implement rate limiting
Add rate limiting to your webhook endpoint to prevent abuse:
Copy
Ask AI
const rateLimit = require('express-rate-limit');const webhookLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1 minute max: 100 // limit each IP to 100 requests per minute});app.post('/webhook', webhookLimiter, (req, res) => { // Your webhook handler});
Validate timestamp window
The default 5-minute window prevents replay attacks. Adjust based on your needs, but don’t make it too large:
Copy
Ask AI
// Default 5 minutes is recommendedverifyWebhookSignature(payload, signature, secret, headers, 5);// For high-security applications, use a shorter windowverifyWebhookSignature(payload, signature, secret, headers, 1);
Log verification failures
Track failed verification attempts to detect potential security issues:
Copy
Ask AI
if (!verifyWebhookSignature(payload, signature, secret, headers)) { console.error('Webhook verification failed', { timestamp: new Date().toISOString(), ip: req.ip, signature: signature, // Don't log the payload as it may contain sensitive data }); res.status(400).send("Invalid signature"); return;}