javascript 82 lines · 3 tabs

Verify Stripe Webhook Signatures with a Raw-Body Express Route

Shared by codesnips Jul 2026
3 tabs
const express = require('express');
const webhookRouter = require('./webhookRouter');
const apiRouter = require('./apiRouter');

const app = express();

// Mount BEFORE express.json() so the raw body survives for signature checks.
app.use('/stripe', webhookRouter);

app.use(express.json());
app.use('/api', apiRouter);

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: 'internal_error' });
});

module.exports = app;
3 files · javascript Explain with highlit

A Stripe webhook endpoint must verify each request's signature against the exact bytes Stripe sent, which is why the raw request body — not a parsed JSON object — is required. Express by default runs express.json() globally and discards the raw buffer, so any downstream stripe.webhooks.constructEvent call fails with a signature mismatch. This snippet solves that by isolating the webhook route so it receives the untouched body while the rest of the app still enjoys JSON parsing.

In app.js, the ordering is deliberate: the Stripe router is mounted before the global express.json() middleware. That guarantees the webhook path is handled by a body parser that preserves raw bytes, and every other route mounted after it still parses JSON normally. Getting this order wrong is the single most common cause of constructEvent failures in production.

In webhookRouter.js, express.raw({ type: 'application/json' }) captures the body as a Buffer and hands it directly to stripe.webhooks.constructEvent, along with the stripe-signature header and the endpoint secret. If verification throws, the route returns 400 immediately — a rejected event should never reach business logic. Because Stripe retries on non-2xx responses, the handler acknowledges with res.json({ received: true }) as soon as the work is durably recorded, rather than after slow processing.

Idempotency matters because Stripe can deliver the same event more than once, and network timeouts often trigger retries even when the first delivery succeeded. The recordAndDispatch helper in eventStore.js uses the immutable event.id as a unique key, and INSERT ... ON CONFLICT DO NOTHING lets the database enforce exactly-once semantics without application-level locking. When rowCount is 0, the event was already seen and processing is skipped.

The handler switches on event.type, extracting the typed data.object for known events and ignoring the rest quietly. Heavy work is deliberately deferred to a queue via enqueueFulfillment so the HTTP response stays fast and within Stripe's timeout window. This pattern — verify, dedupe, acknowledge, then process asynchronously — is the standard way to build a webhook receiver that is both secure and resilient to retries.


Related snips

ruby
timestamp = request.headers.fetch('X-Signature-Timestamp')
signature = request.headers.fetch('X-Signature')
payload = request.raw_post

data = "#{timestamp}.#{payload}"
expected = OpenSSL::HMAC.hexdigest('SHA256', ENV.fetch('WEBHOOK_SECRET'), data)

HMAC signed API requests for webhook and partner integrity

hmac api-signing webhooks
by Kai Nakamura 2 tabs
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
go
package dbutil

import (
  "context"

  "github.com/jackc/pgconn"

Retry Postgres serialization failures with bounded attempts

go postgres transactions
by Leah Thompson 1 tab
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
ruby
module EmailNormalization
  extend ActiveSupport::Concern

  included do
    attr_accessor :soft_warnings

Soft Validation: Normalize + Validate Email

rails activerecord validations
by codesnips 4 tabs
ruby
event_id = request.headers.fetch('X-Event-Id')
timestamp = request.headers.fetch('X-Signature-Timestamp').to_i

raise ActionController::BadRequest, 'stale request' if Time.now.to_i - timestamp > 300
raise ActionController::BadRequest, 'replay detected' if WebhookEvent.exists?(external_id: event_id)

Secure webhook endpoint design with replay protection

webhooks replay-protection hmac
by Kai Nakamura 1 tab

Share this code

Here's the card — post it anywhere.

Verify Stripe Webhook Signatures with a Raw-Body Express Route — share card
Link copied