NestJS raw body controller to verify an AI video webhook signature
Enable rawBody in NestFactory, read req.rawBody in a controller and check Sume's sume-v1 HMAC before you act on a job.completed video event.

In NestJS, verify a video webhook by creating the app with rawBody: true and reading req.rawBody, a Buffer of the exact bytes received, in the controller. NestJS documents both: pass { rawBody: true } to NestFactory.create, then type the request as RawBodyRequest<Request>. The docs also warn that this does not work when bodyParser is set to false (NestJS raw body docs, read 2026-10-06).
Sume signs <timestamp>.<raw_body> with HMAC SHA-256 and sends the result as x-sume-webhook-signature: sume-v1=<hex> with the timestamp in x-sume-webhook-timestamp (Sume webhooks guide, read 2026-10-06). A re-serialized @Body() object will not match, which is why the raw buffer matters.
What goes in main.ts and the controller?
Set callback_url on POST /v1/videos to the HTTPS route below. The controller returns 401 when the secret is unset instead of comparing against an empty string.
// main.ts
const app = await NestFactory.create(AppModule, { rawBody: true });
// sume.controller.ts
import { createHmac, timingSafeEqual } from 'node:crypto';
import { Controller, Post, Req, Headers, HttpCode,
UnauthorizedException, RawBodyRequest } from '@nestjs/common';
import { Request } from 'express';
@Controller('hooks')
export class SumeController {
@Post('sume') @HttpCode(200)
handle(@Req() req: RawBodyRequest<Request>,
@Headers('x-sume-webhook-timestamp') ts = '',
@Headers('x-sume-webhook-signature') sig = '') {
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? '';
if (!secret || !req.rawBody || !/^\d+$/.test(ts)) throw new UnauthorizedException();
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new UnauthorizedException();
const want = Buffer.from('sume-v1=' + createHmac('sha256', secret)
.update(`${ts}.`).update(req.rawBody).digest('hex'));
const ok = sig.split(',').some((p) => {
const got = Buffer.from(p.trim());
return got.length === want.length && timingSafeEqual(got, want);
});
if (!ok) throw new UnauthorizedException();
return { received: true };
}
}What can go wrong with rawBody?
bodyParser: falsein the factory options:req.rawBodystays undefined and every delivery is rejected. NestJS says raw body support does not work with it.- A global middleware that parses JSON before Nest does can change what you see; keep the Nest default parser or scope custom ones away from this route.
- Length check before
timingSafeEqual: Node throws when the buffers differ in length, so compare lengths first, as above. - Rotation: the header can hold several comma separated
sume-v1=entries, one per live secret; thesomeloop accepts any match.
What should the handler do after the check?
Return 2xx fast and push the work to a queue. The payload is {event, request_id, job_id, status, payload.artifacts[]} with event one of job.completed, job.failed, job.canceled. Fetch the file with GET /v1/videos/{job_id}/content?index=0 and your API key; the artifact URLs are not public. Keep status polling as a backup for missed deliveries, and key your own record on job_id so a duplicate delivery is a no-op.
Sources
Related posts
More in Developers
- Never set toleranceSeconds to 0 on a Sume webhook verifier
toleranceSeconds 0 skips the timestamp check, so a captured delivery verifies forever. See the replay and a WebCrypto verifier that rejects stale ones.
- A Node CLI to submit a Sume video job: util.parseArgs and --dry-run
A Node script with util.parseArgs that validates duration, builds the /v1/videos request, and prints it with --dry-run before anything bills. Tested offline.
- Node fetch to Sume with Ideogram 4.5: branch on status 200 or 202
POST /v1/images returns 200 with data[].url or 202 with a job envelope. A Node 22 fetch sample that checks res.status, with a 40 second abort signal.
- Node fetch worker pool: submit transcription jobs with retry-after
A 30-line Node 18+ worker pool that posts Sume STT jobs, sleeps for retry-after on 429, and sends an Idempotency-Key per clip. Tested against a stub.
Written by Sume