Using Payment Links, Payment Page, payment extensions, Payouts, POS, or the Reconciliation API? View the classic product documentation
Flutter SDK
Integrate Peach Orchestration SDK into your Flutter app
Minimum: Flutter 3.0+, Dart 2.17+
Prerequisites
Before you start integrating, make sure you have a suitable Flutter toolchain, Peach Orchestration credentials, a backend server that can create Payment Intents, and both native projects set up.
Development environment
- Flutter 3.0 or later.
- Dart 2.17 or later.
- For iOS: Xcode 14+ and iOS 15.1+.
- For Android: Android Studio and minSdk 24+.
Peach Payments account
Don't have an account yet? Sign up first.
Already have an account? Collect your credentials from the Dashboard:
Log in to the Dashboard
You can log in to the Dashboard using one of the following methods:
- Using a magic link; this is the default login method. After entering your email address, Dashboard sends you a magic link that you click to log in.
- Using a password.
Info
Log in using a magic link
To access the Dashboard:
- Visit https://dashboard.peachpayments.com.
- Enter your company email address and click Submit.
- In your email inbox, click the Login to Dashboard link that you received from Peach Payments (if you've got multiple login emails, click the latest link). If the email isn't in your inbox, check your spam folder. The magic link expires after five minutes, but you can redo this procedure to receive a new link.
- When the Dashboard prompts you for two-factor authentication:
- On your phone, open your authenticator app and note the 6-digit authentication code for Peach Payments.

Example Google Authenticator authentication code. - In your browser, in the Verify Your Identity window, enter the authentication code.
- Select Remember this device for 30 days if you don't want the Dashboard to ask for an authentication code for the next 30 days.
- Click Continue.
- On your phone, open your authenticator app and note the 6-digit authentication code for Peach Payments.
Warning
Log in using a password
When you first gain access to the Dashboard, the email contains a link to either log in using a magic link, or to complete your account by setting a password. Use the password that you set there for all future logins, unless you reset it. If you can't find the initial email, you can set your first password using the reset your password functionality.
To access the Dashboard:
- Visit https://dashboard.peachpayments.com.
- Click Username & Password beneath the Submit button.
- Enter your company email address and password, and click Log in.
- When the Dashboard prompts you for two-factor authentication:
- On your phone, open your authenticator app and note the 6-digit authentication code for Peach Payments.
- In your browser, in the Verify Your Identity window, enter the authentication code.
- Select Remember this device for 30 days if you don't want the Dashboard to ask for an authentication code for the next 30 days.
- Click Continue.
- In the left sidebar, click Peach Orchestration.
- Under Peach Orchestration credentials, copy your profile ID (
pro_xxx) and publishable key (pk_snd_xxxfor sandbox,pk_prd_xxxfor live). The publishable key is safe to use in client-side code. - Under API keys, click New API key + to create a merchant API key (
snd_xxxfor sandbox,prd_xxxfor live).
Never ship your merchant API key
The merchant API key belongs on your server only. Never expose it in client-side code — a web page, a mobile app bundle, or anything else a shopper can read.
Can't find these credentials, or don't see the Peach Orchestration section? Contact support to request access.
Backend server
You need a server to create Payment Intents securely.
Never expose your secret key
Your secret key belongs on your server only. Never put it in the Flutter app — anything shipped in an app bundle can be read.
Platform setup
- iOS: run
pod installin theios/directory. - Android: ensure
minSdkVersionis 21 or later inandroid/app/build.gradle.
Wallet prerequisites
Mobile SDK V2 requires your own wallet credentials for app review, unlike the Web SDK, where you can use the Peach Payments certificates and IDs. Use the Apple Pay steps when targeting iOS, and the Google Pay and Samsung Pay steps when targeting Android.
Apple Pay
For app integrations you provide your own Apple Pay certificates, which requires Apple Developer Program membership. Peach Payments provides the certificate signing requests (CSRs) — contact support to request them.
- Register a merchant ID in the Apple Developer portal under Certificates, Identifiers & Profiles > Identifiers > Merchant IDs, using the format
merchant.com.yourcompany. Go to Merchant IDs. - Create a payment processing certificate against that merchant ID.
- In Xcode, go to Signing & Capabilities > + Capability > Apple Pay and tick your registered merchant IDs.
- Send Peach Payments your Apple Pay merchant ID and certificates so they can configure your account.
Apple's own references: Setting up your server and the Apple Pay implementation guide.
Google Pay
For app integrations you provide your own Google Pay ID for app review. The API merchantId is only enforced in production.
- Register in the Google Pay & Wallet Console and send Peach Payments the merchant ID Google issues.
- Complete the business profile — name, logo, and website — that is shown on the payment sheet.
- Submit for production access before going live. The test environment works without approval.
Google's own references: Web integration and Android integration.
Samsung Pay
For app integrations you provide your own Samsung Pay certificates for your app review process.
- Register at Samsung Pay Developers and create a service in the Samsung Pay portal.
- Choose the Online / In-App service type, which generates a service ID.
- Peach Payments provides a CSR so you can obtain the service certificate and register your callback and domain details — contact support to request it.
Samsung's own references: the Samsung Pay console and Getting started.
Add the Peach Payments packages to your pubspec.yaml, then finish the native setup on each platform.
The packages
Published versions differ by package. peachpayments_flutter_airborne is not published (publish_to: none).
| Package | What it is |
|---|---|
peachpayments_flutter | The SDK: payment sheet and saved payment methods. |
peachpayments_flutter_netcetera_3ds | Optional — links Netcetera 3-D Secure. |
peachpayments_flutter_scancard | Optional — links card scanning. |
peachpayments_flutter_airborne | Not published (publish_to: none). |
Add them to pubspec.yaml:
dependencies:
peachpayments_flutter: ^1.4.0
peachpayments_flutter_netcetera_3ds: ^1.1.3 // optional
peachpayments_flutter_scancard: ^1.1.3 // optionalThen install:
flutter pub getNames in code
| What | Name |
|---|---|
| Dart class | PeachPayments |
| Import | package:peachpayments_flutter/peachpayments_flutter.dart |
| Android namespace | io.peachpayments.flutter |
iOS setup
In ios/Podfile:
platform :ios, '15.1'The plugin pulls in peachpayments-hyperswitch-ios/sentry. Your Podfile still needs both pod sources — see the iOS page.
Android setup
In android/app/build.gradle:
android {
defaultConfig {
minSdkVersion 21
}
}Run the Android setup step, and re-run it after every upgrade:
dart run peachpayments_flutter:apply_pluginsBreaking: MainActivity must extend FlutterFragmentActivity
Your Android MainActivity must extend FlutterFragmentActivity, not
FlutterActivity. The wallet button attaches a fragment and will otherwise
fail.
class MainActivity : FlutterFragmentActivity()Complete the setup
Run the app on each platform to complete setup:
flutter runCreate a PeachPayments instance and initialise it once, in main.dart or wherever your app starts up.
Basic initialisation
import 'package:peachpayments_flutter/peachpayments_flutter.dart';
// Create an instance and initialise in main.dart or app startup
final _peach = PeachPayments();
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
_peach.init(HyperConfig(
publishableKey: 'pk_test_your_publishable_key',
customBackendUrl: 'https://app.sandbox-next.peachpayments.com/api',
));
runApp(MyApp());
}Environment-based configuration
Use different keys and hosts for sandbox and production:
class AppConfig {
static const bool isProduction = bool.fromEnvironment('dart.vm.product');
static String get publishableKey =>
isProduction ? 'pk_live_xxx' : 'pk_test_xxx';
static String get apiBaseUrl =>
isProduction ? 'https://app.next.peachpayments.com/api' : 'https://app.sandbox-next.peachpayments.com/api';
}A basic integration is four steps: create a Payment Intent on your server, initialise the payment session, present the payment sheet, and handle the result.
The checkout screen
import 'package:flutter/material.dart';
import 'package:peachpayments_flutter/peachpayments_flutter.dart';
final _peach = PeachPayments();
class CheckoutScreen extends StatefulWidget {
final int amount;
final String currency;
const CheckoutScreen({
Key? key,
required this.amount,
required this.currency,
}) : super(key: key);
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
bool _isLoading = false;
String? _error;
Future<void> _startPayment() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
// Step 1: Create Payment Intent on your server
final checkoutData = await _createPaymentIntent();
// Step 2: Initialise Payment Session
await _peach.initPaymentSession(PaymentSheetParams(
clientSecret: checkoutData.clientSecret,
// Add other required parameters
));
// Step 3: Present Payment Sheet
final result = await _peach.presentPaymentSheet();
// Step 4: Handle result
_handlePaymentResult(result);
} catch (e) {
setState(() => _error = e.toString());
} finally {
setState(() => _isLoading = false);
}
}
Future<CheckoutData> _createPaymentIntent() async {
final response = await http.post(
Uri.parse('${AppConfig.apiBaseUrl}/create-payment-intent'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'amount': widget.amount,
'currency': widget.currency,
}),
);
final data = jsonDecode(response.body);
return CheckoutData.fromJson(data);
}
void _handlePaymentResult(Map<String, dynamic>? result) {
if (result != null && result['status'] == 'succeeded') {
_showSuccess();
} else if (result != null && result['status'] == 'failed') {
setState(() => _error = result['message'] ?? 'Payment failed');
}
// null or cancelled - user dismissed
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Checkout')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
// Order summary
Card(
child: ListTile(
title: Text('Total'),
trailing: Text(
'${widget.currency} ${(widget.amount / 100).toStringAsFixed(2)}',
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
SizedBox(height: 24),
// Error message
if (_error != null)
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(_error!, style: TextStyle(color: Colors.red)),
),
Spacer(),
// Pay button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _startPayment,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text('Pay Now'),
),
),
],
),
),
);
}
}A null result, like a cancelled one, means the shopper dismissed the sheet rather than that the payment failed.
Data models
class CheckoutData {
final String clientSecret;
final String? customerId;
final String? ephemeralKey;
CheckoutData({
required this.clientSecret,
this.customerId,
this.ephemeralKey,
});
factory CheckoutData.fromJson(Map<String, dynamic> json) {
return CheckoutData(
clientSecret: json['client_secret'],
customerId: json['customer_id'],
ephemeralKey: json['ephemeral_key'],
);
}
}The payment sheet shows the methods you have configured, and you can also embed a branded wallet button directly in your own checkout.
Configuring payment methods
Available methods depend on your Peach Payments Dashboard configuration.
import 'package:peachpayments_flutter/peachpayments_flutter.dart';
final _peach = PeachPayments();
// Initialise payment session with configuration
await _peach.initPaymentSession(PaymentSheetParams(
clientSecret: clientSecret,
// Add other required parameters
));
// Present payment sheet
final result = await _peach.presentPaymentSheet();Direct wallet buttons
PeachWalletButton embeds a real branded Apple Pay or Google Pay button in your own checkout, with no payment sheet in between. The customer taps it and completes the payment in the wallet's own sheet.
Mount it only after initPaymentSession has completed.
PeachWalletButton(
clientSecret: clientSecret,
type: WalletButtonType.expressCheckout, // or .googlePay / .applePay
googlePay: const GooglePayButtonConfig(
merchantName: 'Your Store',
merchantCountryCode: 'ZA',
),
onReady: (isReady) => setState(() => _walletReady = isReady),
onResult: (result) {
// result.status
},
)Breaking host requirement on Android
Your MainActivity must extend FlutterFragmentActivity, not
FlutterActivity — the button attaches a fragment and will otherwise fail.
class MainActivity : FlutterFragmentActivity()The button may render nothing
The SDK decides availability: the wallet must be enabled for your merchant, the connector must return a session token, and the wallet must be usable on the device. When any of those fail the widget collapses to zero height. That is a normal empty state, not an error — use onReady as the signal that the button is usable, and always keep a card fallback.
Programmatic start
controller.confirm() starts the flow from your own button instead of the branded one.
Handling redirects
Bank payments, 3-D Secure, and similar flows return to your app through a URL scheme, so configure one on both platforms.
In ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>yourapp</string></array>
</dict>
</array>In android/app/src/main/AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourapp" android:host="payment-return" />
</intent-filter>Saved cards let a returning shopper pay with a card they used before, and Setup Intents let you store a card without charging it.
Enabling saved cards
Enable saved cards by providing customer information in the PaymentSheetParams:
import 'package:peachpayments_flutter/peachpayments_flutter.dart';
final _peach = PeachPayments();
await _peach.initPaymentSession(PaymentSheetParams(
clientSecret: clientSecret,
// Add other required parameters
));Setup Intent: save a card without a payment
Future<void> saveCardForLater(String customerId) async {
// 1. Create Setup Intent on your server
final setupData = await api.createSetupIntent(customerId);
// 2. Initialise payment session with Setup Intent
await _peach.initPaymentSession(PaymentSheetParams(
clientSecret: setupData.clientSecret,
// Add other required parameters
));
// 3. Present sheet
final result = await _peach.presentPaymentSheet();
if (result != null && result['status'] == 'succeeded') {
print('Card saved successfully!');
}
}presentPaymentSheet returns a nullable map, so check for dismissal first, then branch on the status, and catch platform errors around the whole flow.
Handling the result
import 'package:peachpayments_flutter/peachpayments_flutter.dart';
final _peach = PeachPayments();
Future<void> processPayment() async {
try {
// presentPaymentSheet returns Map<String, dynamic>?
final result = await _peach.presentPaymentSheet();
if (result == null) {
// User dismissed - not an error
return;
}
final status = result['status'];
if (status == 'succeeded') {
_onPaymentSuccess();
} else if (status == 'failed') {
_onPaymentFailed(result['message']);
} else if (status == 'cancelled') {
// User cancelled
}
} on PlatformException catch (e) {
_handlePlatformError(e);
} catch (e) {
_handleGenericError(e);
}
}A dismissed sheet is not a failure
A null result means the shopper closed the sheet. Treat it as a neutral
outcome and leave them on your checkout rather than showing an error.
Showing errors to the shopper
Fall back to a generic message when the SDK does not supply one, and give the shopper a way to retry.
void _handlePaymentError(String? message) {
final displayMessage = message ?? 'Payment failed.';
_showErrorSnackBar(displayMessage);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
action: SnackBarAction(
label: 'Retry',
textColor: Colors.white,
onPressed: _startPayment,
),
),
);
}Build a PaymentSheetAppearance to control the sheet's colours, shapes, typography, and primary button.
Appearance
PaymentSheetAppearance _createAppearance() {
return PaymentSheetAppearance(
colors: PaymentSheetColors(
primary: Color(0xFF0066FF),
background: Colors.white,
componentBackground: Color(0xFFF5F5F5),
componentBorder: Color(0xFFE0E0E0),
text: Colors.black,
secondaryText: Colors.grey,
placeholderText: Color(0xFF9E9E9E),
icon: Colors.grey,
error: Colors.red,
),
shapes: PaymentSheetShapes(
borderRadius: 12,
borderWidth: 1,
),
typography: PaymentSheetTypography(
fontFamily: 'Roboto',
sizeScaleFactor: 1.0,
),
primaryButton: PaymentSheetPrimaryButton(
colors: PrimaryButtonColors(
background: Color(0xFF0066FF),
text: Colors.white,
),
shapes: PrimaryButtonShapes(
borderRadius: 8,
),
),
);
}Theme-aware appearance
Derive the appearance from the ambient theme so the sheet follows light and dark mode:
PaymentSheetAppearance getAppearance(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return PaymentSheetAppearance(
colors: PaymentSheetColors(
primary: Theme.of(context).primaryColor,
background: isDark ? Color(0xFF1A1A1A) : Colors.white,
text: isDark ? Colors.white : Colors.black,
),
);
}Use the sandbox test cards to exercise each outcome, and cover your checkout screen with widget tests.
Test cards
| Card number | Outcome |
|---|---|
| 4242424242424242 | Success |
| 4000002500003155 | 3DS required |
| 4000000000000002 | Declined |
Unit tests
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
class MockPaymentApi extends Mock implements PaymentApi {}
void main() {
group('CheckoutScreen', () {
late MockPaymentApi mockApi;
setUp(() {
mockApi = MockPaymentApi();
});
testWidgets('shows pay button', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: CheckoutScreen(amount: 6500, currency: 'ZAR'),
),
);
expect(find.text('Pay Now'), findsOneWidget);
});
testWidgets('shows loading on pay tap', (tester) async {
when(mockApi.createPaymentIntent(any))
.thenAnswer((_) async => CheckoutData(clientSecret: 'pi_xxx'));
await tester.pumpWidget(
MaterialApp(
home: CheckoutScreen(amount: 6500, currency: 'ZAR'),
),
);
await tester.tap(find.text('Pay Now'));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
});
}Most Flutter integration problems come down to a stale build, the native project setup, or the order in which you call the SDK.
MissingPluginException
Clean the build and fetch the packages again:
flutter clean && flutter pub getThen rebuild:
flutter runiOS build fails with CocoaPods
Reintegrate the pods:
cd ios
pod deintegrate
pod installAndroid build fails
Check that minSdkVersion is 24 or later in android/app/build.gradle.
The payment sheet doesn't appear
Make sure you await initPaymentSession before calling presentPaymentSheet.
PeachWalletButton fails to attach on Android
MainActivity must extend FlutterFragmentActivity, not FlutterActivity. The button attaches a fragment.
PeachWalletButton renders nothing
This is a normal empty state. The wallet must be enabled for your merchant, the connector must return a session token, and the wallet must be usable on the device. Use onReady as the readiness signal and keep a card fallback. Also make sure you mount the button only after initPaymentSession has completed.
The 3DS redirect doesn't return
Check that the URL scheme is configured on both platforms.
Getting help
Contact support for help.