NestJS raw body for webhook signature verification

In NestJS, pass rawBody: true to NestFactory.create and verify the webhook signature over req.rawBody, a Buffer. Raise the 100kb JSON limit too.

5 min readSume
All posts

To get the raw body in NestJS, pass { rawBody: true } to NestFactory.create() and type the handler's request as RawBodyRequest<Request>; req.rawBody is then a Buffer of the body exactly as it arrived. That buffer is what a webhook signature check needs. For a Sume webhook, verify it with verifyWebhook from @sume-com/sdk, and parse JSON only from the bytes you verified.

NestJS facts come from its Raw body, Controllers, and Exception filters docs; Sume facts come from Verifying webhooks, Run webhooks, and Webhooks. All were read on 2026-09-27. Sume has no NestJS module: the receiver is an ordinary controller. In plain Express the same fix is route middleware, covered in Express raw body for webhook signatures.

Why does the signature check fail on req.body?

Because Nest has already parsed it. Sume signs every delivery with HMAC-SHA256 over <timestamp>.<raw_body>, and a parsed and re-serialized object does not verify: key order and whitespace are part of what was signed. NestJS's own docs name webhook signature verification as one of the most common reasons to read the raw body, since the HMAC needs the unparsed bytes.

The raw body only exists while Nest's built-in global body parser is on, so don't pass bodyParser: false when you create the app.

How do I enable rawBody in NestJS?

Set it where the app is created, and raise the JSON limit in the same place. On Express, Nest's default platform, the body parser limit defaults to 100kb, and useBodyParser() respects the rawBody option.

From NestJS's Raw body docs and Sume's Run webhooks page, read 2026-09-27.
SettingNestJS behaviorFor a Sume webhook
rawBody: true in NestFactory.create()Keeps the unparsed body; needs the built-in body parserRequired: the signature covers the raw bytes
RawBodyRequest<Request>Exposes req.rawBody as a BufferPass the Buffer to verifyWebhook as is
app.useBodyParser("json", { limit })Express default: 100kbSet it above 1 MiB: Sume's run webhook body limit is 1,048,576 bytes
// main.ts
import { NestFactory } from "@nestjs/core";
import type { NestExpressApplication } from "@nestjs/platform-express";
import { AppModule } from "./app.module.js";

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    rawBody: true, // keeps req.rawBody; never pass bodyParser: false
  });
  app.useBodyParser("json", { limit: "2mb" }); // Express default is 100kb
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

How do I verify a Sume webhook in a NestJS controller?

Inject the request with @Req(), verify req.rawBody, and only then parse it. verifyWebhook accepts the body as a string, an ArrayBuffer, or a typed array, so the Buffer passes as is, and it reads headers from a plain object such as Node's req.headers. It is async, returns false instead of throwing, compares in constant time, and enforces a 300-second replay window by default. Nest answers a POST with 201 unless you set @HttpCode(...); any 2xx counts as delivered.

  • Read the secret from the dashboard's Webhooks tab, or from GET /v1/webhooks/signing-secret with a key carrying account:read, and store it as SUME_COM_WEBHOOK_SIGNING_SECRET. The module below refuses to start without it, so an empty secret can never verify.
  • For 24 hours after a secret rotation, the signature header carries two comma-separated sume-v1= entries. verifyWebhook in @sume-com/sdk 0.2.0 accepts either; a check that compares the whole header fails for that window.
import {
  BadRequestException, Controller, HttpCode, Post, Req,
  UnauthorizedException, type RawBodyRequest,
} from "@nestjs/common";
import type { Request } from "express";
import { verifyWebhook } from "@sume-com/sdk";

const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is not set");

@Controller("hooks")
export class SumeWebhookController {
  @Post("sume")
  @HttpCode(204)
  async receive(@Req() req: RawBodyRequest<Request>) {
    if (!req.rawBody) throw new BadRequestException("no raw body");
    const ok = await verifyWebhook({ body: req.rawBody, headers: req.headers, secret });
    if (!ok) throw new UnauthorizedException("bad signature");
    const event = JSON.parse(req.rawBody.toString("utf8")); // the verified bytes
    await recordOnce(event); // your insert-or-ignore on request_id or job_id
  }
}

What should the handler do after it verifies?

Record the event, answer, and do the slow work later. Signed webhooks for Sume video runs covers the full delivery contract; these rules shape the controller:

  • Each attempt gets 10 seconds, and Sume makes up to 10 attempts, so hand rendering or downloads to a queue instead of doing them before the 204.
  • Dedupe on request_id for run webhooks, which repeats on every retry, and on job_id for generation-job webhooks.
  • Route on event, and answer an event type you don't handle with 204, not a 500, so a new event type does not start a retry storm.
  • A receipt over 1 MiB arrives with payload: null and error.result_url; fetch the receipt from there with your API key.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume