All Webhook Errors

Webhook Raw Body Required Error — Causes & Fixes

"Raw body required" is the error message from Stripe's, GitHub's, and Shopify's verification SDKs when you've passed them a parsed JSON object instead of the raw bytes. It's not a wire-level error from a provider — it's a developer-facing error from your verification code, and it's the single most common gotcha in webhook integration.

Why Raw Body Matters

HMAC signature verification computes a hash over the exact byte sequence the provider sent. Any change — a single whitespace difference, a different key order, a re-encoded character — changes the HMAC. JavaScript objects don't preserve key order. JSON re-serialization adds whitespace differently. The bytes you reconstruct from a parsed object are not the bytes the provider signed.

Root Causes

1. express.json() ran before your verification code

Most common case. express.json() as global middleware parses every request body. By the time your webhook handler runs, req.body is already a JS object. JSON.stringify(req.body) produces different bytes than the provider sent.

2. Framework default behavior

Next.js App Router, NestJS, Hono — all parse bodies automatically. You have to opt out per-route to get raw bytes. The opt-out mechanism varies by framework.

3. Logging middleware that reads the body

A request-logging middleware that calls req.body consumes the stream. The webhook handler downstream sees an empty body. This is invisible until you debug specifically — common in setups where the team added structured logging without realizing it would break webhook routes.

4. Reverse proxy body modification

Some reverse proxies modify request bodies (Cloudflare's "minify" features, certain WAF rules, content-encoding negotiation). Disable any body-modification features on webhook routes.

Fix It — Per Framework

Express

// Mount express.raw() ONLY on the webhook route, not globally
app.use('/api', express.json())   // for normal API routes

app.post('/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    // req.body is now a Buffer of the exact request bytes
    const event = stripe.webhooks.constructEvent(
      req.body,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    )
    res.status(200).send('ok')
  }
)

Next.js (App Router)

// app/api/webhooks/stripe/route.ts
import { NextRequest } from 'next/server'

export async function POST(req: NextRequest) {
  const rawBody = await req.text()       // raw bytes as string
  const sig = req.headers.get('stripe-signature')

  const event = stripe.webhooks.constructEvent(
    rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET
  )

  return new Response('ok')
}

Fastify

// Add a content-type parser that hands you the raw buffer
fastify.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (req, body, done) => done(null, body)
)

fastify.post('/webhooks/stripe', async (req, reply) => {
  // req.body is a Buffer
  const event = stripe.webhooks.constructEvent(
    req.body, req.headers['stripe-signature'], secret
  )
  return reply.code(200).send('ok')
})

Hono / Bun / Deno

app.post('/webhooks/stripe', async (c) => {
  const rawBody = await c.req.text()      // raw, not c.req.json()
  const sig = c.req.header('stripe-signature')
  const event = stripe.webhooks.constructEvent(rawBody, sig, secret)
  return c.text('ok')
})

Python (Flask)

@app.post('/webhooks/stripe')
def stripe_webhook():
    raw = request.get_data()      # bytes — NOT request.json
    sig = request.headers.get('stripe-signature')
    event = stripe.Webhook.construct_event(raw, sig, secret)
    return 'ok'

Sanity Check

// Add a debug log to confirm what your handler sees
import crypto from 'node:crypto'

console.log({
  bodyHash: crypto.createHash('sha256').update(req.body).digest('hex').slice(0, 12),
  bodyLen: req.body.length,
  bodyType: req.body.constructor.name,  // should be 'Buffer'
})

If bodyType is 'Object' instead of 'Buffer', your raw-body setup didn't take. If two identical requests show different bodyHash values, something between the wire and your handler is mutating bytes.

How to Reproduce

Send the same fixed payload (from WebhookWhisper's Test Sender) twice to your handler. Both should produce the same body hash and the same HMAC. If they don't, find the middleware that's mutating the body.

Frequently Asked Questions

Can I use express.json() and then JSON.stringify the result for HMAC?

No — JSON.stringify produces different bytes than the original (key order, whitespace). Always read the raw body before any parser.

Why does Vercel / Next.js have so many tutorials with raw-body workarounds?

Because the App Router's auto-parsing makes raw access tricky. The clean fix is `await req.text()` in App Router routes — most workarounds predate this API.

Should I store the raw body permanently for replay?

Storing raw bytes for ≥7 days is a good idea — when something breaks, you'll need them. Compress (gzip) before storing; webhook payloads compress well.

Debug This Error in Real Time

WebhookWhisper captures every webhook request with full headers, body, and timing — so you can see exactly what the provider sent and reproduce the error instantly.

Start Debugging Free
Webhook Raw Body Required — Causes & Fixes (2026) | WebhookWhisper