McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Handle Paystack Webhooks in NestJS
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Handle Paystack Webhooks in NestJS

To handle Paystack webhooks in NestJS, enable rawBody in NestFactory.create, create a WebhookGuard that verifies the HMAC SHA-512 signature, and build a controller that receives the event, returns 200 immediately, and processes it asynchronously.

In this article

  1. 1. Enable Raw Body Parsing
  2. 2. Create a Webhook Signature Guard
  3. 3. Build the Webhook Controller
  4. 4. Register in the Module
  5. 5. Testing Webhooks Locally
  6. 6. Production Notes
  7. 7. Ship Payments Faster

Enable Raw Body Parsing

NestJS does not capture the raw request body by default. You need it for HMAC verification. Enable it in your bootstrap function:

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

async function bootstrap() {
  var app = await NestFactory.create(AppModule, {
    rawBody: true,
  });

  await app.listen(3000);
}
bootstrap();

With rawBody: true, every request object gets a rawBody property containing the unparsed bytes. This is what you hash for signature verification.

Create a Webhook Signature Guard

// src/payment/guards/paystack-webhook.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';

@Injectable()
export class PaystackWebhookGuard implements CanActivate {
  constructor(private configService: ConfigService) {}

  canActivate(context: ExecutionContext): boolean {
    var request = context.switchToHttp().getRequest();
    var signature = request.headers['x-paystack-signature'];

    if (!signature) {
      return false;
    }

    var secret = this.configService.get('PAYSTACK_SECRET_KEY');
    var hash = crypto
      .createHmac('sha512', secret)
      .update(request.rawBody)
      .digest('hex');

    return hash === signature;
  }
}

This guard runs before the controller method. If the signature does not match, NestJS returns a 403 automatically. The controller code never executes for invalid webhooks.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Build the Webhook Controller

// src/payment/webhook.controller.ts
import { Controller, Post, Body, HttpCode, UseGuards } from '@nestjs/common';
import { PaystackWebhookGuard } from './guards/paystack-webhook.guard';
import { PaymentService } from './payment.service';

@Controller('api/webhooks')
export class WebhookController {
  constructor(private paymentService: PaymentService) {}

  @Post('paystack')
  @UseGuards(PaystackWebhookGuard)
  @HttpCode(200)
  async handleWebhook(@Body() event: any) {
    // Process asynchronously
    this.processEvent(event).catch(function(err) {
      console.error('Webhook processing error:', err);
    });

    return { received: true };
  }

  private async processEvent(event: any) {
    var eventType = event.event;
    var data = event.data;

    if (eventType === 'charge.success') {
      await this.paymentService.fulfillOrder(data.reference);
    } else if (eventType === 'subscription.create') {
      await this.paymentService.createSubscription(data);
    } else if (eventType === 'invoice.payment_failed') {
      await this.paymentService.handleFailedRenewal(data);
    }
  }
}

@HttpCode(200) ensures NestJS returns 200 instead of the default 201 for POST requests. Paystack expects a 200 response.

Register in the Module

// src/payment/payment.module.ts
import { Module } from '@nestjs/common';
import { PaymentController } from './payment.controller';
import { WebhookController } from './webhook.controller';
import { PaystackService } from './paystack.service';
import { PaymentService } from './payment.service';
import { PaystackWebhookGuard } from './guards/paystack-webhook.guard';

@Module({
  controllers: [PaymentController, WebhookController],
  providers: [PaystackService, PaymentService, PaystackWebhookGuard],
  exports: [PaystackService],
})
export class PaymentModule {}

Testing Webhooks Locally

Use ngrok to expose your local server:

npx ngrok http 3000

Set the webhook URL in your Paystack dashboard to the ngrok URL plus your webhook path: https://abc123.ngrok.io/api/webhooks/paystack.

For unit tests, create the HMAC signature yourself and pass it in the request header:

import * as crypto from 'crypto';

var payload = JSON.stringify({
  event: 'charge.success',
  data: { reference: 'test_123', amount: 500000, status: 'success' },
});

var hash = crypto
  .createHmac('sha512', 'sk_test_xxx')
  .update(Buffer.from(payload))
  .digest('hex');

// POST to /api/webhooks/paystack with:
// Header: x-paystack-signature: 
// Body: 

Production Notes

  1. HTTPS required. Your webhook URL must use HTTPS in production.
  2. Respond quickly. Return 200 within a few seconds. Use a queue (Bull, RabbitMQ) for heavy processing.
  3. Idempotent handlers. Always check if the event was already processed before taking action.
  4. Log all events. Store every webhook event in a database table for debugging and audit trails.
  5. Monitor delivery. Check the Paystack dashboard webhook logs for failed deliveries.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Enable rawBody option in NestFactory.create to access the raw request bytes for signature verification.
  • ✓Use a custom Guard to verify the HMAC SHA-512 signature. This keeps your controller clean.
  • ✓Return 200 immediately from the controller. Queue heavy processing for background execution.
  • ✓NestJS Guards work perfectly for webhook signature verification. They run before the controller method.
  • ✓Handle events by type: charge.success, subscription.create, invoice.payment_failed are the most common.
  • ✓Make your handlers idempotent. Paystack may retry and send the same event multiple times.

Frequently Asked Questions

Why use a Guard instead of middleware for signature verification?
Guards integrate with NestJS dependency injection, so you can inject ConfigService to get the secret key. Middleware works too, but Guards are the NestJS-idiomatic approach for request validation.
Do I need to exclude the webhook route from CSRF protection?
If you are using CSRF protection (like csurf), yes. Webhook requests come from Paystack servers, not from your frontend, so they will not have a CSRF token. Exclude the webhook route from CSRF checks.
Can I use NestJS queues for webhook processing?
Yes. Bull queues work well with NestJS. Add the event to a queue in the controller, return 200, and process the event in a queue consumer. This ensures Paystack gets a quick response while you handle the business logic asynchronously.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in Svelte and SvelteKit

Accept Paystack payments in SvelteKit using server endpoints for backend logic and Svelte components for the frontend. This guide covers initializing transactions, inline popup checkout, redirect checkout, and verification in a single full-stack SvelteKit project.

Read more

Handle Paystack Webhooks in Flask

Receive and verify Paystack webhooks in your Flask application. This guide covers accessing request.data for raw bytes, HMAC SHA-512 signature verification, processing charge.success events, and making handlers idempotent.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Paystack Transaction References: Design Patterns That Prevent Chaos

Your transaction reference is the single string that ties a Paystack payment to your internal records. A sloppy reference scheme leads to duplicate charges, mystery transactions, and 2 AM debugging sessions. This guide covers UUID, sequential, and composite patterns with code for each.

Read more

Paystack Insufficient Funds Response Handling

When a customer's account does not have enough money, Paystack returns "Insufficient Funds" in the gateway_response. Your job is to detect this, show a tactful message that does not embarrass the customer, and offer alternatives.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.