MarzbanSDK
Webhooks

Fastify

Handling Marzban webhooks in Fastify — raw body access and signature verification.

Fastify does not parse the body by default unless you add a content-type parser. For webhook signature verification you need access to the raw bytes — register a custom parser that preserves them.

Setup

npm install fastify marzban-sdk

Full example

import Fastify from 'fastify'
import { createMarzbanSDK, isWebhookSignatureError } from 'marzban-sdk'

const app = Fastify()

const 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,
  },
})

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

// Register a raw body parser for the webhook route
app.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (_req, body, done) => done(null, body)
)

app.post('/webhook', async (req, reply) => {
  try {
    await sdk.webhook.handleWebhook(
      req.body as Buffer,                        // raw Buffer
      (req.headers['x-signature'] as string)
    )
    reply.send({ ok: true })
  } catch (err) {
    if (isWebhookSignatureError(err)) {
      reply.code(401).send({ error: 'Invalid signature' })
    } else {
      reply.code(400).send({ error: 'Bad webhook payload' })
    }
  }
})

await app.listen({ port: 3000 })
console.log('Fastify webhook server listening on :3000')

Scoped content-type parser

If you only want the raw-buffer parser on the webhook route (to avoid affecting other routes), use a scoped plugin:

import Fastify from 'fastify'

const app = Fastify()

// Regular JSON for all routes
app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) =>
  done(null, JSON.parse(body as string))
)

// Scoped raw parser for /webhook only
app.register(async function webhookPlugin(fastify) {
  fastify.addContentTypeParser(
    'application/json',
    { parseAs: 'buffer' },
    (_req, body, done) => done(null, body)
  )

  fastify.post('/webhook', async (req, reply) => {
    await sdk.webhook.handleWebhook(req.body as Buffer, req.headers['x-signature'] as string)
    reply.send({ ok: true })
  })
})

On this page