To verify a Paystack payment in NestJS, inject your PaystackService and call the verify endpoint with the transaction reference. Check that data.data.status is "success", the amount matches your stored price, and the currency is correct. Use an idempotent database update to prevent double-granting.
Add a typed verification method to your PaystackService:
// In paystack.service.ts
async verifyTransaction(reference: string): Promise<{
success: boolean;
amount?: number;
currency?: string;
email?: string;
reference?: string;
status?: string;
}> {
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{
headers: {
Authorization: 'Bearer ' + this.secretKey,
},
}
);
var data = await response.json();
if (!data.status || data.data.status !== 'success') {
return {
success: false,
status: data.data ? data.data.status : 'unknown',
};
}
return {
success: true,
amount: data.data.amount,
currency: data.data.currency,
email: data.data.customer.email,
reference: data.data.reference,
};
}
This returns a clean typed object your controllers can use without knowing the Paystack API response shape.
Create a dedicated fulfillment method that combines verification with database checks:
// src/payment/payment.service.ts
import { Injectable } from '@nestjs/common';
import { PaystackService } from './paystack.service';
@Injectable()
export class PaymentService {
constructor(private paystackService: PaystackService) {}
async fulfillOrder(reference: string) {
// 1. Look up the order
var order = await this.orderRepository.findOne({ where: { reference } });
if (!order) {
return { error: 'Order not found' };
}
if (order.paid) {
return { success: true, alreadyFulfilled: true };
}
// 2. Verify with Paystack
var result = await this.paystackService.verifyTransaction(reference);
if (!result.success) {
return { error: 'Payment not successful', status: result.status };
}
// 3. Check amount (stored in kobo)
if (result.amount !== order.amountInKobo) {
return { error: 'Amount mismatch' };
}
// 4. Check currency
if (result.currency !== order.currency) {
return { error: 'Currency mismatch' };
}
// 5. Idempotent fulfillment
var updated = await this.orderRepository.update(
{ reference: reference, paid: false },
{ paid: true, paidAt: new Date() }
);
if (updated.affected === 0) {
return { success: true, alreadyFulfilled: true };
}
// 6. Deliver value
await this.sendConfirmationEmail(order.email, order);
return { success: true, alreadyFulfilled: false };
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// In payment.controller.ts
@Get('callback')
async handleCallback(
@Query('reference') reference: string,
@Res() res: Response,
) {
if (!reference) {
return res.redirect('/payment/failed?error=missing_reference');
}
var result = await this.paymentService.fulfillOrder(reference);
if (result.error) {
return res.redirect('/payment/failed?error=' + encodeURIComponent(result.error));
}
return res.redirect('/payment/success?reference=' + reference);
}
@Get('verify')
async verifyPayment(@Query('reference') reference: string) {
if (!reference) {
return { error: 'Missing reference' };
}
return this.paymentService.fulfillOrder(reference);
}
Both the callback handler (for redirect checkout) and the verify endpoint (for inline popup) use the same fulfillOrder method. This ensures consistent verification logic across all payment paths.
Create a custom exception filter for payment-related errors:
// src/payment/filters/payment-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
export class PaymentVerificationException extends HttpException {
constructor(message: string) {
super({ error: 'Payment verification failed', message: message }, 400);
}
}
@Catch(PaymentVerificationException)
export class PaymentExceptionFilter implements ExceptionFilter {
catch(exception: PaymentVerificationException, host: ArgumentsHost) {
var ctx = host.switchToHttp();
var response = ctx.getResponse();
response.status(400).json({
success: false,
error: exception.message,
});
}
}
This gives you a consistent error response format for all payment verification failures.
Unit test the verification by mocking the PaystackService:
// src/payment/__tests__/payment.service.spec.ts
describe('PaymentService', function() {
it('should fulfill order on successful verification', async function() {
var mockPaystackService = {
verifyTransaction: jest.fn().mockResolvedValue({
success: true,
amount: 500000,
currency: 'NGN',
email: 'test@example.com',
reference: 'test_ref',
}),
};
// Inject mock service and test fulfillOrder
});
it('should reject on amount mismatch', async function() {
var mockPaystackService = {
verifyTransaction: jest.fn().mockResolvedValue({
success: true,
amount: 100000, // Different from stored amount
currency: 'NGN',
}),
};
// Verify the service returns an amount mismatch error
});
});
Mock the fetch calls rather than hitting the real Paystack API in tests. Use Paystack test keys for integration tests only.
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