← Blog
·

Express Webhook Handling: Setup, Security, and Testing

Introduction

Express webhook handling is building a POST endpoint in Express.js that receives event notifications from external services, verifies the request, and processes the event safely. If your Node.js app needs to react to payments, GitHub pushes, Shopify order updates, Slack events, or Twilio callbacks, webhooks are a practical way to do it.

Webhooks deliver events as they happen instead of making your server poll for updates. That reduces unnecessary traffic and makes integrations more responsive.

A production webhook receiver follows a simple flow: receive the request, validate the JSON payload, verify the webhook signature, return a fast acknowledgement, and process the event after that. Providers such as Stripe, GitHub, Shopify, Slack, and Twilio expect a quick response and a reliable verification step.

This guide covers secure, production-ready Express webhook handling. If you want the basics first, see what a webhook is. You’ll build a receiver that can handle real providers with confidence, including the logging and verification practices that keep webhook processing reliable.

What are webhooks and why use Express.js for them?

A webhook is an event-driven HTTP callback: when something happens in a provider, it sends a POST request to your endpoint with event data. For example, GitHub can notify your app about a push, Stripe can send a payment event, and Shopify can send an order update. For a plain-language overview, see what a webhook is.

Webhooks are better than polling because your app does not keep asking for updates on a schedule. The provider sends data only when the event occurs, which is more efficient and reduces unnecessary traffic.

Express.js fits webhook handling well because it keeps setup minimal while giving you flexible routing and middleware for parsing, logging, and verification. In Node.js backends, Express is lightweight, familiar, and easy to extend, which makes it a strong choice for Express webhook handling.

The main tradeoff is security: webhook verification often depends on the raw body and signature checks, so you must configure Express carefully. A webhook route should also return the right HTTP status codes, usually a fast 2xx response, so providers know the event was received.

Set up an Express webhook server

Start with a Node.js project and install the basics:

npm init -y
npm install express dotenv

Express.js gives you the HTTP server and routing, while dotenv loads local environment variables from a .env file so you can keep provider settings and webhook secrets out of source code. Never hardcode secrets like Stripe signing keys or GitHub webhook tokens; they should live in environment variables for safer local development and easier deployment.

A minimal server.js can expose a dedicated webhook route and a health check:

require('dotenv').config();
const express = require('express');

const app = express();

app.use('/webhooks', express.raw({ type: 'application/json' }));
app.get('/health', (_, res) => res.send('ok'));

app.post('/webhooks/provider', (req, res) => {
  res.sendStatus(200);
});

app.listen(process.env.PORT || 3000);

For cleaner setup, split routes, services, and utilities as the app grows. Use middleware for parsing, verification, and logging, and keep provider-specific config in .env files. See Express webhook handling for the full pattern.

Create, validate, and secure the webhook endpoint

Create a dedicated POST route for webhook traffic, such as /webhooks/stripe, instead of reusing general API routes. In Express.js, keep the handler thin: confirm the method, check required headers, validate the JSON payload shape, then return a fast 2xx response before any slow downstream work. Use request validation to confirm fields like event_type, event_id, timestamp, and data exist and have the expected types; libraries like Zod or Joi help, but defensive checks still matter.

For signature verification, providers sign the raw body with a shared secret. They typically use HMAC with SHA-256: the provider hashes the exact request bytes plus the secret, and your server recomputes the hash and compares it safely with timingSafeEqual. If Express parses the body first, the bytes can change and verification fails. When supported, reject stale timestamps to reduce replay attack risk. Common failures include a wrong secret, altered payload, bad signature header format, or a parsed-body mismatch. See Express webhook handling and webhook security best practices.

Store webhook secrets in environment variables and load them with dotenv during development. In production, keep them in your deployment platform’s secret store, rotate them when providers support it, and restrict access to only the services that need them. Use HTTPS for every webhook endpoint so the signature and payload are not exposed in transit.

Verify a webhook signature in Node.js

A typical Node.js verification flow looks like this:

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

This example shows the core idea, but each provider formats signatures differently. Stripe, for example, includes a timestamped header format; GitHub uses an HMAC signature header; Shopify and Slack also have provider-specific verification rules. Always follow the provider’s documentation for the exact header name, encoding, and timestamp checks.

The reason the raw body matters is that signature verification must use the exact bytes the provider sent. If Express converts the payload into a JavaScript object first, whitespace, key order, or encoding changes can make the computed signature differ from the original.

Handle retries, idempotency, and duplicate events

Webhook providers retry deliveries when they do not get a successful HTTP status code, usually a 2xx response. That means duplicate events are normal in webhook processing, so your handler must tolerate the same payload more than once. Idempotency means repeated processing produces the same final result, like marking an order paid once even if Stripe sends the event again. Store processed event IDs in your database, Redis, or a durable log, and check status before acting.

For slow or failure-prone work, push the payload into a queue and finish with background jobs so the endpoint stays fast. Use safe repeated actions like upserts, status checks, and conditional updates. Add logging and monitoring to spot retry storms, repeated signature failures, or downstream outages.

Webhook retries usually happen on timeouts, network errors, or non-2xx responses. Providers often retry with backoff and may stop after a limited number of attempts, so your endpoint should acknowledge valid events quickly and make processing safe to repeat.

Test webhooks locally with ngrok

Use ngrok to expose your local Express server, for example ngrok http 3000, then copy the public URL into the provider’s callback URL field. Trigger a sample event from the provider dashboard or test tool, then confirm your endpoint returns the expected HTTP status codes and logs the request metadata.

Test the happy path, then force failures: send an invalid webhook signature, a malformed payload, and a request that should trigger provider retries. For practical checklists, see webhook testing best practices, testing webhooks effectively, and testing webhook endpoints.

Should webhook processing be synchronous or asynchronous?

In most production systems, webhook processing should be asynchronous after the initial verification step. The endpoint should do the minimum work needed to validate the request and return a 2xx response, then hand the event to a queue or worker for heavier processing.

Synchronous processing is acceptable only when the work is tiny and deterministic, such as recording a delivery ID or writing a small audit log. It becomes risky when the handler calls external APIs, performs long database transactions, or sends notifications, because those steps can slow the response and trigger retries.

A good pattern is: verify the signature, validate the payload, persist the event, enqueue the job, and return immediately. The worker can then perform the business logic, retry safely, and update status in the background.

How do I validate incoming webhook payloads?

Validate webhook payloads in two layers. First, check the transport-level details: required headers, HTTP method, content type, and signature fields. Second, validate the JSON payload structure and business rules.

For example, you might require an event_id, event_type, created_at, and data object. Then validate that the event type is one you support, the timestamp is within an acceptable range, and the nested fields match the provider’s schema. Libraries like Zod, Joi, or Ajv can help, but you should still reject unknown or malformed events early.

Payload validation is important because a valid signature only proves the request came from the provider; it does not prove the payload is useful, complete, or safe for your application logic.

Best practices for secure webhook handling

Secure webhook handling starts with a few non-negotiables:

  • Use HTTPS for every endpoint.
  • Verify the webhook signature against the raw body.
  • Store secrets in environment variables or a secret manager.
  • Reject stale timestamps when the provider includes them.
  • Add rate limiting to reduce abuse.
  • Validate the payload before processing.
  • Keep the endpoint narrow and dedicated to webhook traffic.
  • Log delivery IDs, event types, and failure reasons.

These controls help defend against spoofed requests, replay attack attempts, accidental duplicates, and downstream failures. For a deeper checklist, see webhook security best practices.

How do I monitor webhook failures in production?

Monitoring should tell you when deliveries fail, why they fail, and whether the failures are isolated or systemic. Log the provider name, delivery ID, event type, response status, processing duration, and any verification error. Track metrics for signature failures, non-2xx responses, queue depth, retry counts, and worker errors.

Set alerts for spikes in failed deliveries, repeated signature verification failures, and growing background job backlogs. If you use Stripe, GitHub, Shopify, Slack, or Twilio, compare provider delivery logs with your application logs so you can identify whether the issue is in the provider, the network, or your code.

Common mistakes to avoid

The most common webhook bugs in Express webhook handling come from skipping the details that make delivery reliable. Forgetting raw body access breaks signature verification because providers usually sign the exact bytes they sent, not the parsed JSON object. Doing slow synchronous work before sending a response can block the event loop, cause timeouts, and trigger retries that create duplicate deliveries. Returning non-2xx responses unnecessarily has the same effect, and ignoring duplicate deliveries can lead to double charges, duplicate records, or repeated notifications.

Another common mistake is using the wrong secret or the wrong signature format for the provider. Stripe, GitHub, Shopify, Slack, and Twilio all have slightly different verification rules, so copy the provider’s exact header and hashing requirements instead of assuming one format fits all.

Conclusion

The secure workflow is simple: receive the POST request, validate the payload shape, verify the signature against the raw body, return a fast 2xx response, then process the event asynchronously when needed. If the work is expensive, hand it off to a queue or background jobs worker so the webhook endpoint stays fast and predictable. Pair that with idempotency, so the same event can arrive more than once without causing harm.

For a stronger implementation, compare your route against Express webhook handling, review webhook security best practices, and tighten your test cases with webhook testing best practices, testing webhooks effectively, and testing webhook endpoints.

Get started with ReqPour

Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.