Verify Paystack Payments in Flutter
Flutter cannot call the Paystack verify endpoint directly (it requires a secret key). After the WebView checkout, extract the transaction reference from the callback URL and send it to your backend. Your backend calls GET https://api.paystack.co/transaction/verify/:reference and returns the result. Show the payment status in Flutter based on your backend response.
Backend: Verify Endpoint
Your Flutter app calls this endpoint after checkout. The backend calls Paystack:
// Node.js/Express backend
app.get('/api/pay/verify', async (req, res) => {
var reference = req.query.reference;
if (!reference) return res.status(400).json({ error: 'Reference required' });
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY }
}
);
var data = await response.json();
if (!data.status || data.data.status !== 'success') {
return res.status(400).json({
verified: false,
status: data.data?.status || 'error',
message: data.data?.gateway_response || 'Payment not verified'
});
}
res.json({
verified: true,
amount: data.data.amount / 100,
currency: data.data.currency,
reference: data.data.reference,
customer_email: data.data.customer.email
});
});
Flutter: Payment Service
// lib/services/payment_service.dart
import 'package:http/http.dart' as http;
import 'dart:convert';
class PaymentResult {
final bool verified;
final double? amount;
final String? currency;
final String? reference;
final String? errorMessage;
const PaymentResult({
required this.verified,
this.amount,
this.currency,
this.reference,
this.errorMessage,
});
}
class PaymentService {
static const String _baseUrl = 'https://your-backend.com';
static Future<PaymentResult> verify(String reference) async {
try {
var response = await http.get(
Uri.parse('$_baseUrl/api/pay/verify?reference=$reference'),
headers: {'Content-Type': 'application/json'},
).timeout(const Duration(seconds: 15));
var data = jsonDecode(response.body);
if (response.statusCode == 200 && data['verified'] == true) {
return PaymentResult(
verified: true,
amount: (data['amount'] as num).toDouble(),
currency: data['currency'],
reference: data['reference'],
);
} else {
return PaymentResult(
verified: false,
errorMessage: data['message'] ?? 'Verification failed',
);
}
} catch (e) {
return PaymentResult(
verified: false,
errorMessage: 'Network error: ${e.toString()}',
);
}
}
}
Verification Screen in Flutter
// lib/payment_result_screen.dart
import 'package:flutter/material.dart';
import 'services/payment_service.dart';
class PaymentResultScreen extends StatefulWidget {
final String reference;
const PaymentResultScreen({super.key, required this.reference});
@override
State<PaymentResultScreen> createState() => _PaymentResultScreenState();
}
class _PaymentResultScreenState extends State<PaymentResultScreen> {
late Future<PaymentResult> _verifyFuture;
@override
void initState() {
super.initState();
_verifyFuture = PaymentService.verify(widget.reference);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<PaymentResult>(
future: _verifyFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Verifying payment...'),
],
),
);
}
if (!snapshot.hasData || !snapshot.data!.verified) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(snapshot.data?.errorMessage ?? 'Payment failed'),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Try Again'),
),
],
),
);
}
var result = snapshot.data!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, size: 64, color: Colors.green),
const SizedBox(height: 16),
const Text('Payment Successful', style: TextStyle(fontSize: 24)),
const SizedBox(height: 8),
Text('${result.currency} ${result.amount?.toStringAsFixed(2)}'),
const SizedBox(height: 8),
Text('Ref: ${result.reference}', style: const TextStyle(color: Colors.grey)),
],
),
);
},
),
);
}
}
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Verification must happen on your backend — never call the Paystack verify endpoint from Flutter with a secret key.
- ✓Extract the reference from the callback URL in the WebView NavigationDelegate, then pass it to your backend.
- ✓Your backend verifies with Paystack and returns a clean result to Flutter. Flutter shows success or failure UI.
- ✓Always handle network errors in Flutter — mobile networks are unreliable and verification can fail due to connectivity.
- ✓Store verification status in your local database (SQLite/Hive) to avoid re-verifying on app restart.
- ✓Use FutureBuilder or a loading state to show a progress indicator while verification is in progress.
Frequently Asked Questions
- Can Flutter call the Paystack verify API directly?
- No. The verify endpoint requires your secret key in the Authorization header. Putting a secret key in Flutter code is insecure — it would be exposed in the compiled app. Always call verify through your backend, which keeps the secret key on the server.
- What if the Flutter app crashes or closes during verification?
- Store the transaction reference locally (SharedPreferences or SQLite) before opening the WebView. On app restart, check for pending references and re-run verification. Also, your backend should receive a webhook from Paystack, which is more reliable than the mobile verification flow.
- How do I handle verification on slow mobile networks?
- Set a timeout on the HTTP call (15 seconds is reasonable). Show a retry button in the error state. If the timeout fires, the payment may still have succeeded — do not show a failure message immediately. Instead, show "Checking your payment..." and offer a retry.
- Should I show the Paystack transaction reference to the user?
- Yes, showing the reference is helpful for customer support. If a user contacts you about a payment issue, having the reference lets you look it up in the Paystack dashboard. Store it in your database and show it on the receipt screen.
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