MarzbanSDK
Webhooks

NestJS

Handling Marzban webhooks in NestJS with RawBodyRequest and signature verification.

NestJS parses JSON bodies globally by default. To verify webhook signatures you need to enable rawBody: true in the app factory and use RawBodyRequest in your controller.

Setup

npm install @nestjs/core @nestjs/common @nestjs/platform-express marzban-sdk

Enable raw body

// main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    rawBody: true, // required for webhook signature verification
  })
  await app.listen(3000)
}
bootstrap()

SDK module (singleton)

// marzban/marzban.module.ts
import { Module, Global } from '@nestjs/common'
import { MarzbanService } from './marzban.service'

@Global()
@Module({
  providers: [MarzbanService],
  exports: [MarzbanService],
})
export class MarzbanModule {}
// marzban/marzban.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common'
import { createMarzbanSDK, MarzbanSDK } from 'marzban-sdk'

@Injectable()
export class MarzbanService implements OnModuleInit {
  sdk!: MarzbanSDK

  async onModuleInit() {
    this.sdk = await createMarzbanSDK({
      baseUrl: process.env.MARZBAN_URL!,
      username: process.env.MARZBAN_USER!,
      password: process.env.MARZBAN_PASS!,
      webhook: {
        secret: process.env.MARZBAN_WEBHOOK_SECRET,
      },
    })

    this.sdk.webhook.on('user_created', payload => {
      console.log('User created:', payload.user.username)
    })

    this.sdk.webhook.on('user_limited', payload => {
      console.log('User limited:', payload.username)
    })
  }
}

Webhook controller

// webhook/webhook.controller.ts
import {
  Controller,
  Post,
  Req,
  Res,
  HttpCode,
  Headers,
} from '@nestjs/common'
import type { RawBodyRequest } from '@nestjs/common'
import type { Request, Response } from 'express'
import { MarzbanService } from '../marzban/marzban.service'
import { isWebhookSignatureError, isWebhookValidationError } from 'marzban-sdk'

@Controller('webhook')
export class WebhookController {
  constructor(private readonly marzban: MarzbanService) {}

  @Post()
  @HttpCode(200)
  async handleWebhook(
    @Req() req: RawBodyRequest<Request>,
    @Res() res: Response,
    @Headers('x-signature') signature: string,
  ) {
    try {
      await this.marzban.sdk.webhook.handleWebhook(req.rawBody!, signature)
      res.json({ ok: true })
    } catch (err) {
      if (isWebhookSignatureError(err)) {
        res.status(401).json({ error: 'Invalid signature' })
      } else if (isWebhookValidationError(err)) {
        res.status(400).json({ error: 'Invalid payload' })
      } else {
        throw err
      }
    }
  }
}

Wire everything up

// app.module.ts
import { Module } from '@nestjs/common'
import { MarzbanModule } from './marzban/marzban.module'
import { WebhookController } from './webhook/webhook.controller'

@Module({
  imports: [MarzbanModule],
  controllers: [WebhookController],
})
export class AppModule {}

req.rawBody is a Buffer when rawBody: true is set in NestFactory.create. It is undefined if you forget to enable that option.

On this page