Flutter — quickstart
A complete, runnable Flutter checkout. Copy each file into a fresh
flutter create project, replace the placeholders, and flutter run.
What we’re building
Section titled “What we’re building”A button on the home screen that:
- Calls your backend to mint a Pdirect session token.
- Pushes a
PdirectPayCheckoutwidget that walks the customer through a wallet payment. - Prints the result to the console.
The merchant secret stays on your backend. The SDK only ever sees
the per-checkout session_token.
1 — Backend stub
Section titled “1 — Backend stub”If you don’t already have a backend with a /checkout/start
endpoint, here’s a 30-line Node example you can spin up locally:
// server.ts (run with: npx tsx server.ts)import http from "node:http";import crypto from "node:crypto";
const KEY_ID = process.env.PDIRECT_MERCHANT_KEY_ID!;const SECRET = process.env.PDIRECT_MERCHANT_SECRET!;const BASE = "https://app.api.gtwy.pdirect.com";
const server = http.createServer(async (req, res) => { if (req.method !== "POST" || req.url !== "/checkout/start") { res.writeHead(404).end(); return; } const r = await fetch(`${BASE}/api/v1/internal/sessions/create`, { method: "POST", headers: { Authorization: `Bearer ${KEY_ID}:${SECRET}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ amount: "12.50", currency: "usd", customer_reference: "cust_abc123", ttl_seconds: 1800, }), }); res.writeHead(r.status, { "Content-Type": "application/json" }); res.end(await r.text());});
server.listen(8080, () => console.log("→ http://localhost:8080/checkout/start"));Run it:
PDIRECT_MERCHANT_KEY_ID=mch_3f9a2e1b PDIRECT_MERCHANT_SECRET=sk_test_... npx tsx server.tsFor Android emulator, the host machine is at 10.0.2.2. For iOS
simulator, localhost works.
2 — Initialise the SDK
Section titled “2 — Initialise the SDK”import 'package:flutter/material.dart';import 'package:pdirect_pay/pdirect_pay.dart';
void main() { PdirectPay.init(const PdirectPayConfig( environment: PdirectPayEnvironment.sandbox, defaultLocale: 'en', )); runApp(const MyApp());}
class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Pdirect demo', home: HomeScreen(), ); }}3 — Mint a session and launch the widget
Section titled “3 — Mint a session and launch the widget”import 'dart:convert';import 'package:flutter/material.dart';import 'package:http/http.dart' as http;import 'package:pdirect_pay/pdirect_pay.dart';
class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState();}
class _HomeScreenState extends State<HomeScreen> { bool _busy = false;
Future<String> _startCheckout() async { // Android emulator: http://10.0.2.2:8080. iOS sim / desktop: localhost. final r = await http.post( Uri.parse('http://10.0.2.2:8080/checkout/start'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({}), ); if (r.statusCode != 201) { throw Exception('mint failed: ${r.statusCode} ${r.body}'); } return jsonDecode(r.body)['session_token'] as String; }
Future<void> _onPay() async { setState(() => _busy = true); try { final sessionToken = await _startCheckout(); if (!mounted) return;
await Navigator.of(context).push( MaterialPageRoute( fullscreenDialog: true, builder: (_) => PdirectPayCheckout( configs: PdirectPayConfigs( sessionToken: sessionToken, acceptLanguage: 'en', ), paymentBody: const PdirectPaymentBody( // amount / currency / customerReference are bound to // the session at mint time — omit them from the body. paymentMethod: 'wallet', userInfo: 'full', paymentMethodInfo: 'full', feeCoveredBy: 'buyer', deliveryBehaviour: 'direct_delivery', notifyUrl: 'https://merchant.example.com/pdirect/webhook', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', phoneNumber: '+243998857000', walletNumber: '3005710720108954', walletPin: '048698', ), onResponse: (r) { debugPrint('payment ok: ${r.transactionId} ${r.paymentStatus.name}'); }, onError: (e) { debugPrint('payment failed: ${e.errorCode?.name} ${e.message}'); }, ), ), ); } catch (e, st) { debugPrint('checkout error: $e\n$st'); } finally { if (mounted) setState(() => _busy = false); } }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Pdirect demo')), body: Center( child: ElevatedButton.icon( onPressed: _busy ? null : _onPay, icon: _busy ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.lock_outline), label: const Text('Pay \$12.50'), ), ), ); }}4 — Run it
Section titled “4 — Run it”flutter runTap Pay $12.50. The SDK pushes the checkout widget; in sandbox
the wallet flow resolves to approved within a few seconds and
onResponse fires with a transaction_id.
What just happened
Section titled “What just happened”- Your Flutter app called your backend at
/checkout/start. - Your backend authenticated to Pdirect with the long-lived
merchant_secretand asked for a session bound to(12.50, usd, cust_abc123). - The gateway minted a 30-min
session_tokenand returned it once. Your backend forwarded it to the Flutter app. - The Flutter SDK presented the token as
Authorization: Bearer …on/payments/collectand/payments/submit. It also auto-attachedIdempotency-KeyandX-Device-Fingerprintheaders. - The wallet processor approved synchronously.
onResponsefired withpaymentStatus: approvedand atransactionIdyou can reconcile against your order.
Going beyond wallet
Section titled “Going beyond wallet”The same pattern applies to every payment method — only the
paymentMethod and the method-specific fields change. See:
- Payment methods — the field-by-field reference
- Card payments with 3DS
- Mobile money status polling
See also
Section titled “See also”- Configuration — every option
- API reference — direct service classes when you need finer control
- Error handling