Browse documentation

Webhook examples

NestJS webhook example

Configure raw-body access and signature verification in a NestJS controller.

DevelopersAdvancedAvailable

What this guide covers

  • Enable raw request-body access for the webhook route.
  • Verify the signature before passing the event to application services.
  • Deduplicate event processing before producing business side effects.

Enable NestJS raw body support

For the Express adapter, pass rawBody: true when creating the Nest application. Nest then exposes the captured Buffer through RawBodyRequest<Request>.rawBody.

src/main.ts
ts
import { NestFactory } from "@nestjs/core"
import type {
  NestExpressApplication,
} from "@nestjs/platform-express"
import { AppModule } from "./app.module"

async function bootstrap() {
  const app =
    await NestFactory.create<NestExpressApplication>(
      AppModule,
      { rawBody: true }
    )

  await app.listen(process.env.PORT ?? 3000)
}

void bootstrap()

Verify in the controller

src/yanola-webhook.controller.ts
ts
import {
  BadRequestException,
  Controller,
  Post,
  type RawBodyRequest,
  Req,
} from "@nestjs/common"
import type { Request } from "express"
import {
  verifyYanolaWebhookSignature,
} from "@yanola/whatsapp"

@Controller("webhooks/yanola")
export class YanolaWebhookController {
  @Post()
  async receive(
    @Req() request: RawBodyRequest<Request>
  ): Promise<void> {
    const signingSecret =
      process.env.YANOLA_WEBHOOK_SECRET

    if (!signingSecret || !request.rawBody) {
      throw new BadRequestException(
        "Webhook verification is not configured"
      )
    }

    const valid = verifyYanolaWebhookSignature({
      secret: signingSecret,
      signature: request.get("yanola-signature"),
      rawBody: request.rawBody,
    })

    if (!valid) {
      throw new BadRequestException(
        "Invalid Yanola signature"
      )
    }

    const event = JSON.parse(
      request.rawBody.toString("utf8")
    )
    await acceptEventOnce(event)
  }
}

Register the controller

src/app.module.ts
ts
import { Module } from "@nestjs/common"
import {
  YanolaWebhookController,
} from "./yanola-webhook.controller"

@Module({
  controllers: [YanolaWebhookController],
})
export class AppModule {}

Return after durable acceptance

A controller that resolves without an explicit response normally returns a 2xx Nest response. Ensure acceptEventOnce writes to a durable, uniquely keyed store before the method resolves.

  1. Put a unique constraint on the evt_ event ID.
  2. Treat duplicate inserts as already accepted.
  3. Queue slow handlers after the durable write.
  4. Use an exception only when Yanola should treat the attempt as failed.

Guards, pipes, and interceptors

Keep signature verification at the webhook boundary before business pipes or interceptors trust the parsed payload. Authentication intended for dashboard users should not block Yanola's signed server-to-server request.

Safe request logging can include yanola-event-id and yanola-delivery-id, but must exclude the secret, full signature, signed raw payload, and sensitive customer content.

Verify the integration

  1. Create an enabled Test Mode endpoint for /webhooks/yanola.
  2. Store its copy-once secret as YANOLA_WEBHOOK_SECRET.
  3. Send yanola.webhook.test from the dashboard.
  4. Confirm the controller stores one event and returns 2xx.
  5. Repeat or replay the event and confirm no duplicate side effect occurs.