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 secret that serves as your signing key.Each webhook request includes the signature under the header name: X-Hook0-Signature.
Unix timestamp (seconds) when the webhook was sent
1728394718
v0
HMAC-SHA256 over {t}.{body} — protects the body and timestamp only
9f8e7d6c5b4a...
h
Space-separated, ordered list of header names included in the v1 signature
content-type x-event-id x-event-type
v1
HMAC-SHA256 over {t}.{h}.{headerValues}.{body} — also binds the listed headers
a1b2c3d4e5f6...
v0 and v1 are both included on every request. v0 includes the timestamp and body; v1 additionally binds the listed headers. Unless you want to bind the headers, which is unnecessary for most use cases, use v0.
Verification process
Extract signature components: Parse the t, h, v0, and v1 values from the header
Build signed payload: Concatenate timestamp.headerNames.headerValues.rawBody
Compute expected signature: Create HMAC-SHA256 hash using your secret
Compare signatures: Use timing-safe comparison to match expected vs. provided
Verify timestamp: Ensure the webhook isn’t too old (prevents replay attacks)
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
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 the subscription response) 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:
// ✅ 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:
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:
// 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:
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;}