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.

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.
| Setting | NestJS behavior | For a Sume webhook |
|---|---|---|
rawBody: true in NestFactory.create() | Keeps the unparsed body; needs the built-in body parser | Required: the signature covers the raw bytes |
RawBodyRequest<Request> | Exposes req.rawBody as a Buffer | Pass the Buffer to verifyWebhook as is |
app.useBodyParser("json", { limit }) | Express default: 100kb | Set 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-secretwith a key carryingaccount:read, and store it asSUME_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.verifyWebhookin@sume-com/sdk0.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_idfor run webhooks, which repeats on every retry, and onjob_idfor generation-job webhooks. - Route on
event, and answer an event type you don't handle with204, not a500, so a new event type does not start a retry storm. - A receipt over 1 MiB arrives with
payload: nullanderror.result_url; fetch the receipt from there with your API key.
Sources
Related posts
More in Integrations
- Open WebUI MCP server: connect Sume's hosted MCP
Add Sume's hosted MCP to Open WebUI as an admin: type MCP (Streamable HTTP), then OAuth 2.1 per user or one shared Bearer key.
- OpenAI Agents SDK MCP server: Sume and the 5-second timeout
Connect the OpenAI Agents SDK to Sume's hosted MCP server with an API key, and raise the 5-second client timeouts above jobs_wait's 55 seconds.
- OpenAI Responses API MCP tool: call Sume's hosted tools
Add Sume's hosted MCP server to the Responses API as an mcp tool, send your Sume key in headers, and approve paid tool calls before they run.
- MCP server for OpenClaw: add Sume's image and video tools
Save Sume's hosted MCP server in OpenClaw with openclaw mcp add, sign in with OAuth or an API-key header, and set requestTimeoutMs above 55,000.
Written by Sume