Skip to content
Blog

Stripe Payments in Flutter: A Practical Integration Guide

A hands-on guide to taking card payments in Flutter with flutter_stripe — setup on Android and iOS, PaymentSheet flow, test cards, and security rules for handling card data.

Published on • October 29, 2026

AI Assistant

Card payments sit at the worst intersection in mobile development: one mistake and you’re handling regulated data you never wanted to see. The good news is that Flutter’s ecosystem has a mature, official-feeling path through it. The flutter_stripe package (14.2.0 at time of writing, published by the verified flutterstripe.io publisher) gives you Stripe’s native PaymentSheet, Apple Pay, Google Pay, SCA-ready 3D Secure, and PCI-compliant card collection on Android and iOS.

This guide walks through the whole integration: installing the package, configuring the publishable key and platform setup, collecting a card payment with PaymentSheet, testing with test cards, and the security do’s and don’ts that keep you out of trouble.

Before you start: where the money actually moves

Two architectural rules drive everything below:

  1. Your server creates the PaymentIntent; the app only confirms it. The secret key never ships in a binary.
  2. Card data goes directly to Stripe. The package’s stated design goal is that “sensitive data is sent directly to Stripe instead of passing through your server” — which is what keeps you inside PCI-compliant boundaries.

Also check the platform policy angle before you build: the flutter_stripe README notes that if you sell digital goods or services inside your app (subscriptions, in-game currency, premium content), Apple and Google guidelines generally require their in-app purchase APIs instead (with US developers treated differently under a 2025 court ruling). Stripe is for physical goods, services, and other allowed scenarios.

Install

dart pub add flutter_stripe

That adds flutter_stripe plus the federated stripe_android / stripe_ios implementation packages.

Configure the publishable key

Only publishableKey is required (stripeAccountId, threeDSecureParams, and merchantIdentifier are optional). Set it early, before any payment UI:

import 'package:flutter_stripe/flutter_stripe.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  Stripe.publishableKey = stripePublishableKey; // pk_test_... while developing

  runApp(const App());
}

Use test-mode keys while developing and live keys only when you publish.

Android setup

The Stripe Android SDK has hard requirements — miss one and PaymentSheet fails at runtime:

  1. Android 5.0 (API level 21)+, Kotlin 1.8.0+, Android Gradle Plugin 8+.
  2. Your activity must extend FlutterFragmentActivity instead of FlutterActivity, because the SDK needs the Support Fragment Manager:
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()
  1. Use a theme that is a descendant of Theme.AppCompat (both normal and values-night variants).
  2. Add the ProGuard rules from the README to proguard-rules.pro:
-dontwarn com.stripe.android.pushProvisioning.**
-dontwarn com.google.android.gms.tapandpay.**
-dontwarn kotlinx.parcelize.Parceler$DefaultImpls
-dontwarn kotlinx.parcelize.Parceler
-dontwarn kotlinx.parcelize.Parcelize
# Keep Stripe classes
-keep class com.stripe.** { *; }
  1. Rebuild the app — these changes do not apply through hot reload.

iOS setup

  • Target iOS 13+ (platform :ios, '13.0' in the Podfile, or IPHONEOS_DEPLOYMENT_TARGET in Xcode).
  • For card scanning, add NSCameraUsageDescription to Info.plist.
  • For redirect-based methods (iDEAL, PayPal, Link), register a URL scheme shared by CFBundleURLSchemes, Stripe.urlScheme, and your PaymentSheet returnURL so the SDK can receive the callback.

Create the PaymentIntent on your server

Your backend talks to the Stripe API with the secret key and returns only the client secret (plus customer/ephemeral key details if you use saved cards). Stripe’s docs require endpoints on your server that use an official Stripe library — the underlying call is a POST /v1/payment_intents, which looks like this with Dart’s http package:

// Server-side endpoint (shelf / dart_frog / your backend of choice)
final response = await http.post(
  Uri.parse('https://api.stripe.com/v1/payment_intents'),
  headers: {'Authorization': 'Bearer $stripeSecretKey'}, // server-only secret key
  body: {
    'amount': '1499', // $14.99 — smallest currency unit
    'currency': 'usd',
  },
);
final clientSecret = jsonDecode(response.body)['client_secret'] as String;
return {'paymentIntent': clientSecret};

The app fetches this from your endpoint — never from Stripe directly.

Collect the payment with PaymentSheet

flutter_stripe offers three ways to take cards — PaymentSheet, CardField, and CardForm — but the README is explicit: Stripe is sunsetting CardField and CardForm in favor of PaymentSheet, which is recommended for all new integrations. It ships localization, animations, and error handling out of the box, plus Apple Pay and Google Pay.

Initialize once, before presenting:

Future<void> initPaymentSheet() async {
  try {
    // 1. PaymentIntent client secret from YOUR server
    final data = await fetchPaymentIntentClientSecret();

    // 2. Initialize
    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        paymentIntentClientSecret: data['paymentIntent'],
        merchantDisplayName: 'Flutter Stripe Store Demo',
        customerId: data['customer'],
        customerEphemeralKeySecret: data['ephemeralKey'],
        returnURL: 'flutterstripe://redirect',
        primaryButtonLabel: 'Pay now',
        applePay: PaymentSheetApplePay(merchantCountryCode: 'US'),
        googlePay: PaymentSheetGooglePay(
          merchantCountryCode: 'US',
          testEnv: true, // keep true while testing
        ),
        style: ThemeMode.dark,
      ),
    );
  } catch (e) {
    rethrow;
  }
}

Then confirm from your checkout button:

Future<void> confirmPayment() async {
  try {
    await Stripe.instance.presentPaymentSheet();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Payment successfully completed')),
    );
  } on StripeException catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Error from Stripe: ${e.error.localizedMessage}')),
    );
  }
}

The SDK performs native 3D Secure authentication automatically when Strong Customer Authentication requires it — you don’t build that flow yourself.

Handle success server-side. Wait for the payment_intent.succeeded webhook rather than trusting the client callback: a user can close the app before the client ever reports back. Fulfil orders from the webhook, not from presentPaymentSheet() returning.

Test with test cards

Test in test mode with test API keys. From Stripe’s testing docs:

ScenarioCard numberDetails
Successful Visa payment4242 4242 4242 4242Any future date (e.g. 12/34), any 3-digit CVC
Successful Mastercard5555 5555 5555 4444Any future date, any 3-digit CVC
Generic decline4000 0000 0000 0002Returns card_declined / generic_decline
Insufficient funds4000 0000 0000 9995Returns insufficient_funds
In API codepm_card_visaPrefer PaymentMethod tokens over raw numbers server-side

Rules worth memorizing:

  • Never use real card details. Stripe’s Services Agreement prohibits testing in live mode with real payment method data.
  • Use a valid future expiry and a three-digit CVC (four for Amex).
  • Test error paths too — declines, insufficient funds, and 3DS challenges are where checkout UX usually breaks.
  • In server-side test code, prefer pm_card_visa-style PaymentMethods over card numbers; raw numbers in server code can create PCI scope when you go live.

Security do’s and don’ts

Do:

  • Keep the secret key on the server, in environment variables or a secrets vault — never in the repo or app bundle.
  • Let flutter_stripe handle card entry via Stripe’s native UI, so raw card data flows directly to Stripe.
  • Use Stripe.publishableKey (safe to embed) in the app, and swap to live keys only at release.
  • Fulfil orders from webhooks (payment_intent.succeeded), verified server-side.
  • Set testEnv: true in Google Pay params until you ship.

Don’t:

  • Don’t read, log, store, or transmit PAN/CVC in your own code or backend — that’s the fastest route to losing PCI compliance.
  • Don’t commit live keys in .env files checked into version control.
  • Don’t treat the client’s “payment succeeded” message as proof of payment.
  • Don’t ignore platform rules: digital goods inside the app generally belong to StoreKit/Google Play Billing.

Conclusion & next steps

You’ve wired the full loop: dart pub add flutter_stripe, publishable key at startup, FlutterFragmentActivity and AppCompat on Android, iOS 13+ target, a server-created PaymentIntent, initPaymentSheet + presentPaymentSheet in the app, webhook-based fulfilment, and test cards for every scenario.

Next steps: add saved cards with customerEphemeralKeySecret, enable Apple Pay/Google Pay wallets, run the package’s example app (cp lib/.env.example.dart lib/.env.dart) against your own test keys, and add stripe listen --forward-to localhost:4242/webhook during local development.

Sources