Bonaventure OgetoBy Bonaventure Ogeto|

Accept Payments with Paystack in Flutter

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.

Flutter + Paystack Architecture

Payment flow for a Flutter app:

  1. Flutter app calls your backend API (not Paystack directly)
  2. Backend initializes transaction with Paystack secret key, returns authorization_url
  3. Flutter opens the authorization_url in a WebView
  4. Customer completes payment on Paystack-hosted page
  5. Paystack redirects to your callback_url
  6. Flutter detects the redirect, extracts the reference
  7. Flutter calls your backend to verify the transaction
  8. Backend verifies with Paystack, returns result to Flutter

Your Flutter app never touches the Paystack secret key. It only knows the transaction reference.

Add Dependencies

# 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" />

Backend: Initialize Transaction

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,
  });
});

Flutter: WebView Checkout

// 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!')),
    );
  }
}

Learn More

Key Takeaways

  • Never put your Paystack secret key in Flutter code. Always initialize transactions from your backend.
  • Use paystack_flutter package for an easy setup, or webview_flutter for a custom WebView approach.
  • After the WebView redirects to your callback URL, extract the reference and verify on your backend.
  • Amounts must be in the smallest currency unit (kobo for NGN) — always multiply by 100 on the backend.
  • The popup approach works in Flutter by loading the Paystack hosted checkout in a WebView.
  • Add INTERNET permission to AndroidManifest.xml and configure NSAppTransportSecurity in iOS Info.plist.

Frequently Asked Questions

Should I use paystack_flutter package or a custom WebView?
The paystack_flutter package handles the WebView setup for you and is maintained by the community. A custom WebView with webview_flutter gives you more control. For most apps, paystack_flutter is faster to set up. For custom checkout experiences, use the WebView approach in this guide.
Can I use Paystack Inline.js popup in Flutter?
Not directly. Flutter renders on a canvas, not a web browser. You must use a WebView to display any web-based Paystack UI. The WebView approach in this guide loads the Paystack hosted checkout page, which is effectively the same user experience.
How do I handle the callback URL on mobile?
Set your callback_url to a real HTTPS URL on your server. In the WebView NavigationDelegate, intercept requests that start with your callback URL. Extract the reference query parameter and close the WebView. You do not need to handle the callback on the server for mobile — just extract the reference from the URL and verify on your backend.
Does Paystack work for both Android and iOS Flutter apps?
Yes. The WebView approach using webview_flutter works on both platforms. Ensure you add INTERNET permission on Android and configure NSAppTransportSecurity in iOS Info.plist to allow HTTPS connections to paystack.co.

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