Flutter Stripe Integration Tutorial: Build a Payment App

by Didin J. on Aug 27, 2025 Flutter Stripe Integration Tutorial: Build a Payment App

Learn how to integrate Stripe with Flutter to build a secure payment app. Step-by-step guide with backend setup, payment intents, and testing.

In today’s digital economy, accepting payments online is a must-have feature for many applications. Stripe has become one of the most popular payment gateways thanks to its simplicity, security, and support for multiple payment methods. On the other hand, Flutter makes it easy to build beautiful, cross-platform apps for iOS and Android with a single codebase.

In this tutorial, we’ll combine the two and show you how to integrate Stripe with Flutter to build a secure payment app. You’ll learn how to set up a Flutter project, connect it with Stripe, handle payments via a backend, and test everything using Stripe’s test environment. By the end, you’ll have a working Flutter payment app that can serve as the foundation for e-commerce, subscriptions, or in-app purchases.

Prerequisites

Before we start, make sure you have the following installed and set up:

  • Flutter SDK (latest stable version) → Install Guide

  • Android Studio or VS Code with Flutter/Dart plugins

  • Stripe account (sign up for free at Stripe)

  • Basic knowledge of Flutter and Dart programming

  • Backend environment (we’ll use Node.js/Express in this tutorial, but you can also use Firebase Cloud Functions or any server-side stack you prefer).

You’ll also need to copy your Stripe Publishable Key and Secret Key from the Stripe Dashboard under Developers → API Keys.


Project Setup

First, let’s create a new Flutter project and add the required dependencies.

Step 1: Create a New Flutter Project

Open your terminal and run the following command:

flutter create flutter_stripe_payment

Navigate into the project folder:

cd flutter_stripe_payment

Open it with your preferred IDE (Android Studio or VS Code).

Step 2: Add Required Dependencies

We’ll use the following Flutter packages:

  • flutter_stripe → Official Stripe Flutter SDK.

  • http → To call our backend API.

  • provider (optional) → For simple state management.

Open pubspec.yaml and add:

dependencies:
  flutter:
    sdk: flutter
  flutter_stripe: ^11.0.0
  http: ^1.2.2
  provider: ^6.1.2

Then, run:

flutter pub get

Step 3: Configure iOS and Android

The Stripe SDK requires some additional setup for iOS and Android.

iOS Setup

Open ios/Runner/Info.plist and add the following:

<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>remote-notification</string>
</array>

Also, update the deployment target to at least iOS 13 in ios/Podfile:

platform :ios, '13.0'

Then run:

cd ios
pod install
cd ..

Android Setup

Open android/app/build.gradle and ensure minSdkVersion is set to 21 or higher:

android {
    defaultConfig {
        minSdkVersion 21
    }
}

Step 4: Initialize Stripe

In lib/main.dart, initialize Stripe with your publishable key (replace pk_test_xxx with your own from Stripe Dashboard):

import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';

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

  // Set the publishable key
  Stripe.publishableKey = "pk_test_xxx";

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Stripe Payment',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const PaymentScreen(),
    );
  }
}

class PaymentScreen extends StatelessWidget {
  const PaymentScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text("Stripe Payment App Setup Complete"),
      ),
    );
  }
}

Run the app to make sure everything compiles correctly:

flutter run

Flutter Stripe Integration Tutorial: Build a Payment App - setup

At this point, the Flutter project is set up with the Stripe SDK and ready to integrate payments.


Backend Setup – Create Payment Intent with Stripe (Node.js + Express)

Stripe requires a server-side backend to create payment intents securely, because the secret key must never be exposed in your Flutter app. The backend will handle creating a PaymentIntent and returning its clientSecret to the Flutter app.

Step 1: Initialize Node.js Project

Create a new folder for your backend:

mkdir stripe-backend
cd stripe-backend
npm init -y

Step 2: Install Dependencies

We’ll use Express for API routing and the Stripe Node.js SDK to interact with Stripe.

npm install express stripe cors dotenv
  • express → lightweight web framework

  • stripe → Stripe SDK

  • cors → allow requests from Flutter app

  • dotenv → manage environment variables

Step 3: Configure Stripe Keys

Create a file named .env in your backend project root:

STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxx
PORT=5000

⚠️ Use your Stripe Secret Key (from Dashboard → Developers → API Keys).

Step 4: Create Express Server

Create a file named server.js:

require("dotenv").config();
const express = require("express");
const cors = require("cors");
const Stripe = require("stripe");

const app = express();
const stripe = Stripe(process.env.STRIPE_SECRET_KEY);

app.use(cors());
app.use(express.json());

// Test route
app.get("/", (req, res) => {
  res.send("Stripe Payment Backend is Running 🚀");
});

// Create PaymentIntent
app.post("/create-payment-intent", async (req, res) => {
  try {
    const { amount, currency } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount, // in cents (e.g. 1000 = $10)
      currency,
      automatic_payment_methods: { enabled: true },
    });

    res.json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Step 5: Run Backend

Start the server:

node server.js

You should see:

Server running on port 5000

Test the API with Postman or curl:

curl -X POST http://localhost:5000/create-payment-intent \
  -H "Content-Type: application/json" \
  -d '{"amount":1000,"currency":"usd"}'

Expected response:

{
  "clientSecret": "pi_12345_secret_67890"
}

✅ Now the backend is ready! This API will generate a PaymentIntent and return its clientSecret for the Flutter app to confirm the payment.


Flutter Integration – Collect Payment and Confirm with Stripe

Now that we have our backend ready, we can connect the Flutter app to create a payment intent and confirm it with Stripe.

Step 1: Create a Service to Call the Backend

Inside lib/, create a new file: payment_service.dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

class PaymentService {
  final String backendUrl = "http://localhost:5000"; // change if deployed

  Future<String?> createPaymentIntent(int amount, String currency) async {
    try {
      final response = await http.post(
        Uri.parse("$backendUrl/create-payment-intent"),
        headers: {"Content-Type": "application/json"},
        body: jsonEncode({"amount": amount, "currency": currency}),
      );

      if (response.statusCode == 200) {
        final jsonResponse = jsonDecode(response.body);
        return jsonResponse["clientSecret"];
      } else {
        if (kDebugMode) {
          print("Failed to create payment intent: ${response.body}");
        }
        return null;
      }
    } catch (e) {
      if (kDebugMode) {
        print("Error creating payment intent: $e");
      }
      return null;
    }
  }
}

Step 2: Build Payment UI

Let’s create a simple PaymentScreen with a button to trigger a test payment.
Update lib/main.dart:

// ignore_for_file: use_build_context_synchronously

import 'package:flutter/material.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
import 'payment_service.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  Stripe.publishableKey = "pk_test_xxxxxxxxxxxxx"; // replace with your key
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Stripe Payment',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const PaymentScreen(),
    );
  }
}

class PaymentScreen extends StatefulWidget {
  const PaymentScreen({super.key});

  @override
  State<PaymentScreen> createState() => _PaymentScreenState();
}

class _PaymentScreenState extends State<PaymentScreen> {
  final PaymentService _paymentService = PaymentService();
  bool _isLoading = false;

  Future<void> _makePayment() async {
    setState(() => _isLoading = true);

    try {
      // Step 1: Create Payment Intent on backend
      final clientSecret = await _paymentService.createPaymentIntent(
        1000,
        "usd",
      ); // $10

      if (clientSecret == null) {
        throw Exception("Failed to get client secret");
      }

      // Step 2: Initialize payment sheet
      await Stripe.instance.initPaymentSheet(
        paymentSheetParameters: SetupPaymentSheetParameters(
          paymentIntentClientSecret: clientSecret,
          style: ThemeMode.light,
          merchantDisplayName: "Djamware Shop",
        ),
      );

      // Step 3: Present payment sheet
      await Stripe.instance.presentPaymentSheet();

      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text("Payment Successful ✅")));
    } catch (e) {
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(SnackBar(content: Text("Payment Failed: $e")));
    }

    setState(() => _isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Stripe Payment")),
      body: Center(
        child: _isLoading
            ? const CircularProgressIndicator()
            : ElevatedButton(
                onPressed: _makePayment,
                child: const Text("Pay \$10"),
              ),
      ),
    );
  }
}

Step 3: Run the App

Start both the backend and the Flutter app:

# In backend folder
node server.js
# In Flutter project
flutter run

When you tap Pay $10, Stripe will open the Payment Sheet.

Use a Stripe test card like:

4242 4242 4242 4242
Exp: Any future date
CVC: Any 3 digits

If successful, you’ll see a Payment Successful ✅ message, and the payment will be logged in your Stripe Dashboard.

✅ At this point, we’ve integrated Stripe in Flutter to collect and confirm payments using the Payment Sheet


Handle Payment Status & Error Handling

Payments can succeed, fail, or be canceled by the user. To provide a smooth experience, we need to handle all possible outcomes gracefully.

Step 1: Update Payment Flow with Error Handling

Let’s improve _makePayment() in PaymentScreen:

Future<void> _makePayment() async {
  setState(() => _isLoading = true);

  try {
    // Step 1: Create Payment Intent on backend
    final clientSecret =
        await _paymentService.createPaymentIntent(1000, "usd"); // $10

    if (clientSecret == null) {
      throw Exception("Failed to get client secret");
    }

    // Step 2: Initialize payment sheet
    await Stripe.instance.initPaymentSheet(
      paymentSheetParameters: SetupPaymentSheetParameters(
        paymentIntentClientSecret: clientSecret,
        style: ThemeMode.light,
        merchantDisplayName: "Djamware Shop",
      ),
    );

    // Step 3: Present payment sheet
    await Stripe.instance.presentPaymentSheet();

    // Success case
    _showMessage("Payment Successful ✅");
  } on StripeException catch (e) {
    // Stripe-specific errors (canceled, card declined, etc.)
    _showMessage("Payment canceled or failed: ${e.error.localizedMessage}");
  } catch (e) {
    // Other errors
    _showMessage("Something went wrong: $e");
  }

  setState(() => _isLoading = false);
}

void _showMessage(String message) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text(message)),
  );
}

Step 2: Handle Common Scenarios

  1. Successful Payment

    • Show a success Snackbar or redirect to a confirmation screen.

  2. Canceled Payment

    • User closes payment sheet → handled by StripeException.

    • Show a friendly message: “Payment canceled.”

  3. Failed Payment (e.g., card declined)

    • Stripe returns an error message (e.g., “Your card was declined”).

    • Display to the user with _showMessage().

Step 3: Optional – Create a Success Page

You can redirect users to a dedicated success screen after successful payment.

class SuccessScreen extends StatelessWidget {
  const SuccessScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Payment Success")),
      body: const Center(
        child: Icon(Icons.check_circle, color: Colors.green, size: 100),
      ),
    );
  }
}

Update _makePayment() success case:

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => const SuccessScreen()),
);

✅ Now your app gracefully handles success, cancellation, and failure while keeping the user experience smooth.


Testing with Stripe

Before going live, Stripe provides a test mode with sample cards so you can safely verify your payment flow.

Step 1: Use Stripe Test Keys

Make sure your backend .env file is using the test secret key (sk_test_xxx), and your Flutter app is using the test publishable key (pk_test_xxx).

You can find these under:
Stripe Dashboard → Developers → API Keys

Step 2: Test Cards

Stripe provides multiple test card numbers. You can enter them in the Payment Sheet when testing.

Common examples:

Card Number Description
4242 4242 4242 4242 Always succeeds
4000 0027 6000 3184 Requires authentication
4000 0000 0000 9995 Always fails (insufficient funds)

Use any future expiry date and any 3-digit CVC.

Step 3: Run the App

  1. Start your backend: 

    node server.js

     

  2. Run Flutter app: 

    flutter run
  3. Tap Pay $10 → Payment Sheet will appear.

  4. Enter one of the test cards above.

  5. If you use 4242 4242 4242 4242, payment succeeds immediately.

  6. If you use 4000 0027 6000 3184, Stripe may ask for 3D Secure (authentication popup).

  7. If you use 4000 0000 0000 9995, the payment fails

Step 4: Monitor in Stripe Dashboard

Go to Stripe Dashboard → Payments.
You’ll see all your test payments listed with status:

  • Succeeded

  • ⚠️ Requires action

  • Failed

This helps confirm your integration works correctly.

✅ With test mode, you can simulate all possible payment scenarios before moving to production.


Bonus – Save Customer & Payment Methods (Optional)

For many apps (e.g., e-commerce or subscriptions), it’s useful to let users save their payment methods for future use. Stripe allows you to create a Customer and attach payment methods to them.

Step 1: Update Backend to Create Customer

In server.js, add an endpoint for creating a Stripe customer:

// Create Customer
app.post("/create-customer", async (req, res) => {
  try {
    const { email } = req.body;

    const customer = await stripe.customers.create({
      email: email,
    });

    res.json({ customerId: customer.id });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Step 2: Save Payment Method

When creating a PaymentIntent, you can allow the method to be saved for future use. Update the /create-payment-intent endpoint:

app.post("/create-payment-intent", async (req, res) => {
  try {
    const { amount, currency, customerId } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      customer: customerId, // attach to customer
      setup_future_usage: "off_session", // save card for future use
      automatic_payment_methods: { enabled: true },
    });

    res.json({
      clientSecret: paymentIntent.client_secret,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});
  • customerId → Stripe customer ID (from /create-customer).

  • setup_future_usage: "off_session" → tells Stripe to save card for future payments.

Step 3: Flutter – Create Customer First

Update payment_service.dart:

Future<String?> createCustomer(String email) async {
  try {
    final response = await http.post(
      Uri.parse("$backendUrl/create-customer"),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode({"email": email}),
    );

    if (response.statusCode == 200) {
      final jsonResponse = jsonDecode(response.body);
      return jsonResponse["customerId"];
    } else {
      print("Failed to create customer: ${response.body}");
      return null;
    }
  } catch (e) {
    print("Error creating customer: $e");
    return null;
  }
}

Step 4: Use Saved Payment Method (Future Payments)

Later, you can charge the saved customer without showing the payment sheet again:

app.post("/charge-customer", async (req, res) => {
  try {
    const { customerId, amount, currency } = req.body;

    // Use the default saved payment method
    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency,
      customer: customerId,
      off_session: true,
      confirm: true,
    });

    res.json({ success: true, paymentIntent });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

This way, customers can pay in one tap for future purchases.

✅ With this setup, your app now supports customer accounts and saved payment methods — perfect for subscriptions or repeat purchases.


Conclusion

In this tutorial, you learned how to integrate Stripe with Flutter to build a payment app. We started by setting up a Flutter project with the flutter_stripe package, then built a secure Node.js + Express backend to create payment intents with Stripe’s secret key.

On the Flutter side, we implemented a Payment Sheet flow to collect and confirm payments. We also covered error handling, tested with Stripe’s test cards, and optionally extended the app to save customers and payment methods for future transactions.

With this foundation, you can extend the app further by:

  • Adding Apple Pay and Google Pay support.

  • Implementing subscriptions or recurring payments.

  • Deploying the backend to Heroku, Vercel, or AWS Lambda.

  • Integrating with your own business logic (orders, invoices, receipts).

By combining Flutter’s cross-platform power with Stripe’s reliable payments API, you now have the tools to build modern, secure, and scalable payment-enabled apps. 🚀

You can get the full source code on our GitHub.

That's just the basics. If you need more deep learning about Flutter, Dart, or related, you can take the following cheap course:

Thanks!