React Native apps cannot receive Paystack webhooks directly. Build a webhook endpoint on your backend (Express, Django, Laravel, etc.), verify the x-paystack-signature, and update your database. Notify the React Native app via Firebase Cloud Messaging push notifications, a polling endpoint, or deep links from the callback URL.
Paystack webhooks are HTTP POST requests to an HTTPS URL. React Native apps run on mobile devices with no public IP address or domain. Paystack cannot reach your app directly.
The correct architecture:
Paystack → Your Backend (webhook received)
↓
Update Database
↓
Push Notification to React Native App
↓
App Updates UI
// Node.js/Express
var crypto = require('crypto');
app.post('/api/webhooks/paystack',
express.raw({ type: 'application/json' }),
async (req, res) => {
var sig = req.headers['x-paystack-signature'];
var hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(req.body)
.digest('hex');
if (hash !== sig) return res.status(400).send('Invalid signature');
res.sendStatus(200); // Respond immediately
var event = JSON.parse(req.body.toString());
if (event.event === 'charge.success') {
var reference = event.data.reference;
var email = event.data.customer.email;
var amount = event.data.amount / 100;
// Update database
await db.orders.update({ reference }, { status: 'paid', paidAt: new Date() });
// Send push notification to user
var user = await db.users.findByEmail(email);
if (user?.fcmToken) {
await sendPushNotification(user.fcmToken, {
title: 'Payment Confirmed',
body: 'Your payment of NGN ' + amount + ' was received',
data: { event: 'charge.success', reference },
});
}
}
}
);
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// hooks/usePushNotifications.ts
import { useEffect } from 'react';
import * as Notifications from 'expo-notifications';
import { useNavigation } from '@react-navigation/native';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export function usePushNotifications() {
var navigation = useNavigation();
useEffect(() => {
// Register for push notifications
registerForPushNotifications();
// Handle notification received while app is open
var subscription = Notifications.addNotificationReceivedListener(notification => {
var data = notification.request.content.data;
if (data.event === 'charge.success') {
// Update payment status in local state
console.log('Payment confirmed:', data.reference);
}
});
// Handle notification tapped
var responseSubscription = Notifications.addNotificationResponseReceivedListener(response => {
var data = response.notification.request.content.data;
if (data.event === 'charge.success') {
navigation.navigate('OrderDetails', { reference: data.reference });
}
});
return () => {
subscription.remove();
responseSubscription.remove();
};
}, []);
}
async function registerForPushNotifications() {
var { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;
var token = await Notifications.getExpoPushTokenAsync();
// Send token to your backend
await fetch('https://your-backend.com/api/users/push-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.data }),
});
}
// hooks/usePaymentStatus.ts
import { useState, useEffect, useRef } from 'react';
export function usePaymentStatus(reference: string | null) {
var [status, setStatus] = useState<'pending' | 'success' | 'failed'>('pending');
var timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
if (!reference) return;
var attempts = 0;
var maxAttempts = 10; // Poll for 30 seconds (every 3s)
timerRef.current = setInterval(async () => {
attempts++;
var response = await fetch(
'https://your-backend.com/api/pay/status?reference=' + reference
);
var data = await response.json();
if (data.status === 'success') {
setStatus('success');
clearInterval(timerRef.current!);
} else if (data.status === 'failed' || attempts >= maxAttempts) {
setStatus(data.status === 'failed' ? 'failed' : 'pending');
clearInterval(timerRef.current!);
}
}, 3000);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [reference]);
return status;
}
This guide is part of the Paystack integration guides by language and framework series.
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