Back to Blog
Implementation7 min readApril 21, 2026

Webhook Receiver in Node.js: Express Guide

A production-ready Node.js webhook receiver: raw body parsing, HMAC-SHA256 verification, immediate 200 response, async queue processing, and idempotency. Full code included.

W
WebhookWhisper Team
April 21, 2026

Building a webhook receiver in Node.js requires four things done right: raw body capture, signature verification, immediate acknowledgment, and idempotent processing.

The Key Problem: Raw Body Parsing

Express's express.json() middleware parses the body into an object. But signature verification requires the raw bytes exactly as received. Parsing and re-serializing changes whitespace, making the HMAC differ from what the provider computed.

Setup

npm install express

Complete Receiver

import express from 'express'
import crypto from 'crypto'

const app = express()

// Capture raw body BEFORE json parsing
app.use('/webhooks', express.raw({ type: 'application/json' }))
app.use(express.json())

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET

function verifySignature(rawBody, sigHeader) {
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex')
  const actual = Buffer.from(sigHeader.replace('sha256=', ''), 'hex')
  const exp = Buffer.from(expected, 'hex')
  if (actual.length !== exp.length) return false
  return crypto.timingSafeEqual(actual, exp)
}

const processedEvents = new Set()

app.post('/webhooks/github', (req, res) => {
  const sig = req.headers['x-hub-signature-256']
  if (!sig || !verifySignature(req.body, sig)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  const event = JSON.parse(req.body.toString())
  const eventId = req.headers['x-github-delivery']

  res.status(200).json({ received: true })

  if (processedEvents.has(eventId)) return
  processedEvents.add(eventId)
  setImmediate(() => processEvent(event, eventId))
})

async function processEvent(event, eventId) {
  console.log('Processing event:', eventId, event.action)
}

app.listen(3000)

Stripe-Specific: Timestamp Verification

import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)

app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    let event
    try {
      event = stripe.webhooks.constructEvent(
        req.body,
        req.headers['stripe-signature'],
        process.env.STRIPE_WEBHOOK_SECRET
      )
    } catch (err) {
      return res.status(400).send('Webhook signature verification failed')
    }
    res.status(200).json({ received: true })
    setImmediate(() => handleStripeEvent(event))
  }
)

Production Checklist

  • Use express.raw() not express.json() for webhook routes
  • Always use crypto.timingSafeEqual()
  • Store processed event IDs in Redis or a DB table
  • Return 200 before doing any async work
  • Log the raw payload and headers

Use WebhookWhisper during development to get a public URL and inspect every request.

#nodejs#express#webhooks#implementation

Ready to test your webhooks?

Get a free HTTPS endpoint in under 5 seconds — no signup required.

Create Free Account
Webhook Receiver Node.js: Complete Guide with Express (2026) | WebhookWhisper