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.

5 min readSume
All posts

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: false in the factory options: req.rawBody stays 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; the some loop 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

All Developers posts

Written by Sume