Flutter apps cannot call the Paystack API directly with a secret key. Create a backend endpoint that initializes a transaction and returns the authorization_url. Open that URL in a WebView (using the paystack_flutter package or webview_flutter). When Paystack redirects to your callback_url, detect the URL change, extract the reference, and verify on your backend.
Payment flow for a Flutter app:
Your Flutter app never touches the Paystack secret key. It only knows the transaction reference.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
webview_flutter: ^4.0.0 # For custom WebView approach
http: ^1.2.0 # For API calls to your backend
For Android, add internet permission to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Your backend receives the request from Flutter and calls Paystack:
// Node.js/Express example backend
app.post('/api/pay/initialize', async (req, res) => {
var { email, amount } = req.body;
var reference = 'flutter_' + Date.now();
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
amount: Math.round(amount * 100),
reference,
callback_url: 'https://yourapp.com/payment/callback',
}),
});
var data = await response.json();
if (!data.status) return res.status(400).json({ error: data.message });
res.json({
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
});
// lib/payment_screen.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class PaymentScreen extends StatefulWidget {
final String email;
final double amount;
const PaymentScreen({super.key, required this.email, required this.amount});
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
String? _authUrl;
String? _reference;
bool _loading = true;
late WebViewController _controller;
@override
void initState() {
super.initState();
_initializePayment();
}
Future<void> _initializePayment() async {
var response = await http.post(
Uri.parse('https://your-backend.com/api/pay/initialize'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'email': widget.email, 'amount': widget.amount}),
);
var data = jsonDecode(response.body);
if (data['authorization_url'] != null) {
setState(() {
_authUrl = data['authorization_url'];
_reference = data['reference'];
_loading = false;
});
_setupWebView();
}
}
void _setupWebView() {
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (request) {
// Detect callback URL redirect
if (request.url.startsWith('https://yourapp.com/payment/callback')) {
var uri = Uri.parse(request.url);
var reference = uri.queryParameters['reference'] ??
uri.queryParameters['trxref'];
if (reference != null) {
Navigator.of(context).pop({'reference': reference, 'success': true});
}
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
))
..loadRequest(Uri.parse(_authUrl!));
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Complete Payment'),
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop({'success': false}),
),
),
body: WebViewWidget(controller: _controller),
);
}
}
Open the payment screen and handle the result:
// In your checkout page
Future<void> _startPayment() async {
var result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PaymentScreen(
email: userEmail,
amount: 5000.0, // NGN 5,000
),
),
);
if (result != null && result['success'] == true) {
await _verifyPayment(result['reference']);
}
}
Future<void> _verifyPayment(String reference) async {
var response = await http.get(
Uri.parse('https://your-backend.com/api/pay/verify?reference=' + reference),
);
var data = jsonDecode(response.body);
if (data['verified'] == true) {
// Show success screen, update order status
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Payment successful!')),
);
}
}
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