Flutter cannot receive Paystack webhooks directly — webhooks are server-to-server HTTP requests. Build a webhook endpoint on your backend (Node.js, Django, etc.), verify the x-paystack-signature, process the event, and then notify your Flutter app via push notifications (FCM), WebSocket, or the Flutter app polling a status endpoint. The backend is the source of truth.
A Paystack webhook is an HTTP POST request from Paystack's servers to a URL you control. Mobile apps do not have a public IP address or domain. Paystack cannot reach your Flutter app directly.
The correct pattern:
Your backend handles the webhook. Here is a Node.js example that also triggers a push notification:
// Node.js/Express webhook handler
var crypto = require('crypto');
var admin = require('firebase-admin'); // For FCM push notifications
app.post('/api/webhooks/paystack', express.raw({ type: 'application/json' }), async (req, res) => {
var signature = req.headers['x-paystack-signature'];
var hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(req.body)
.digest('hex');
if (hash !== signature) {
return res.status(400).json({ error: 'Invalid signature' });
}
res.sendStatus(200); // Respond immediately
var event = JSON.parse(req.body.toString());
switch (event.event) {
case 'charge.success':
await handleChargeSuccess(event.data);
break;
default:
console.log('Unhandled event:', event.event);
}
});
async function handleChargeSuccess(data) {
var reference = data.reference;
var amount = data.amount / 100;
var customerEmail = data.customer.email;
// Update order in database
var user = await db.users.findByEmail(customerEmail);
// Push to Flutter app via FCM
if (user && user.fcmToken) {
await admin.messaging().send({
token: user.fcmToken,
notification: {
title: 'Payment Confirmed',
body: 'Your payment of NGN ' + amount + ' was successful',
},
data: {
event: 'charge.success',
reference: reference,
amount: String(amount),
},
});
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// lib/main.dart
import 'package:firebase_messaging/firebase_messaging.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Handle background messages
FirebaseMessaging.onBackgroundMessage(_handleBackgroundMessage);
runApp(const MyApp());
}
Future<void> _handleBackgroundMessage(RemoteMessage message) async {
print('Background message: ${message.data}');
}
// In your app widget
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_setupMessaging();
}
Future<void> _setupMessaging() async {
// Get FCM token and send to backend
var token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await _sendTokenToBackend(token);
}
// Handle foreground messages
FirebaseMessaging.onMessage.listen((message) {
var event = message.data['event'];
if (event == 'charge.success') {
var reference = message.data['reference'];
// Update UI, navigate to success screen
_handlePaymentSuccess(reference);
}
});
// Handle notification tap (app in background)
FirebaseMessaging.onMessageOpenedApp.listen((message) {
var event = message.data['event'];
if (event == 'charge.success') {
_navigateToOrderScreen();
}
});
}
Future<void> _sendTokenToBackend(String token) async {
// Send FCM token to your backend so it can push to this device
await http.post(
Uri.parse('https://your-backend.com/api/users/fcm-token'),
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + userToken},
body: jsonEncode({'fcm_token': token}),
);
}
}
If push notifications are not set up yet, polling is a simpler fallback. Poll your backend every few seconds while the user is on a payment-pending screen:
// Poll for payment status
Timer? _pollingTimer;
void _startPolling(String reference) {
_pollingTimer = Timer.periodic(const Duration(seconds: 3), (timer) async {
var status = await _checkPaymentStatus(reference);
if (status == 'success') {
timer.cancel();
_navigateToSuccess();
} else if (status == 'failed') {
timer.cancel();
_showFailure();
}
// Continue polling if status is 'pending'
});
}
@override
void dispose() {
_pollingTimer?.cancel();
super.dispose();
}
Cancel the timer when the screen is disposed to avoid memory leaks.
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