Handle Paystack Webhooks in React Native
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.
Why React Native Cannot Receive Webhooks
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
Backend Webhook Handler
// 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 },
});
}
}
}
);
React Native: Expo Push Notifications
// 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 }),
});
}
Simpler Alternative: Polling
// 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;
}
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Paystack webhooks hit your backend server, not your React Native app.
- ✓Build the webhook handler on Node.js, Django, Laravel, or any backend framework.
- ✓Use Firebase Cloud Messaging (FCM) or Expo Push Notifications to push payment events to React Native.
- ✓For simpler setups, poll a backend status endpoint every few seconds after checkout.
- ✓Deep links from the Paystack callback URL work for detecting redirect checkout completion.
- ✓Always verify the webhook signature on your backend before taking any action.
Frequently Asked Questions
- Can React Native apps receive Paystack webhooks?
- No. Webhooks are sent to server-side HTTPS endpoints. React Native apps run on mobile devices with no public domain. Build a backend webhook handler and use push notifications or polling to inform the app.
- Should I use Expo push notifications or Firebase FCM for Paystack events?
- Both work. Expo Push Notifications are simpler for Expo projects. Firebase FCM gives more control and works for bare React Native. For a new project, Expo push notifications are faster to set up.
- What if the user has notifications disabled?
- Fall back to polling. After the checkout WebView closes, start polling your backend status endpoint every 3-5 seconds until you get a confirmed status. This handles both notification-disabled and foreground app scenarios.
- How do I test webhooks during React Native development?
- Run your backend locally and expose it with ngrok. Set the ngrok URL as your Paystack webhook URL. Your local backend receives the webhook and can push to the running React Native app via Metro/local network. This works well with Expo Go.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon