
A Flutter Stripe integration needs three pieces: one server endpoint, PaymentSheet in the app and one webhook. The parts people get wrong are who confirms the payment, when to fulfil the order and how money moves in a marketplace with Stripe Connect. This guide covers all of it with flutter_stripe, and the server code works the same for React Native and native apps.
Can you use Stripe instead of Apple in-app purchase?
Check this before writing code. Apple's App Review Guideline 3.1.1 says that if you unlock features or functionality in the app, such as subscriptions, premium content or a full version, you must use in-app purchase. Guideline 3.1.3(e) says the opposite for physical goods and services used outside the app: those must use other payment methods, such as Apple Pay or card entry.
| You're selling | Use |
|---|---|
| Food, bookings, tickets, deliveries, marketplace items | Stripe |
| Services delivered in person | Stripe |
| Premium features, content or a subscription inside the app | Apple and Google in-app purchase |
For the second case, see my Flutter in-app purchase guide.
Set up the flutter_stripe package
flutter_stripe (14.2.0 at the time of writing) is published by flutterstripe.io and wraps Stripe's native iOS and Android SDKs. Its README recommends PaymentSheet for new integrations, as Stripe is sunsetting CardField and CardForm.
flutter pub add flutter_stripe- Android: use
FlutterFragmentActivityinstead ofFlutterActivityinMainActivity.kt, and aTheme.AppCompatdescendant for the activity theme. The README lists a few more items (Kotlin and Gradle plugin versions, ProGuard rules). Skipping these is the usual reason PaymentSheet fails to open on Android. - iOS: set the deployment target to 13.0 or higher.
- Set the publishable key at startup. Stripe lists publishable keys as safe to expose; the secret key never goes in the app.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
Stripe.publishableKey = const String.fromEnvironment('STRIPE_PK');
Stripe.merchantIdentifier = 'merchant.com.example.app'; // only for Apple Pay
await Stripe.instance.applySettings();
runApp(const App());
}How the Stripe payment flow works in a mobile app
With PaymentSheet, the app confirms the PaymentIntent when the user taps pay, and the server fulfils the order when Stripe sends payment_intent.succeeded.

- The app asks your server to start checkout for an order.
- The server works out the amount from its own data and creates a PaymentIntent with the secret key.
- The server returns the PaymentIntent's client secret.
- The app calls
initPaymentSheetandpresentPaymentSheet. PaymentSheet collects payment details, handles 3D Secure and confirms the payment. - Stripe sends
payment_intent.succeededto your webhook. That's when you mark the order paid.
Server: create the PaymentIntent
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
app.post("/checkout", async (req, reply) => {
const order = await loadOrderForUser(req.user.id, req.body.orderId);
const intent = await stripe.paymentIntents.create(
{
amount: order.totalMinorUnits, // 1099 = 10.99 in a two-decimal currency
currency: order.currency, // lowercase ISO code, e.g. "eur"
automatic_payment_methods: { enabled: true },
metadata: { orderId: order.id },
},
{ idempotencyKey: `order-${order.id}` }
);
return { clientSecret: intent.client_secret };
});The amount comes from your database, so a modified client can't change the price. The idempotency key stops a retried request from creating a second PaymentIntent.
Flutter: show PaymentSheet
import 'package:flutter_stripe/flutter_stripe.dart';
Future<void> pay(String orderId) async {
final clientSecret = await api.startCheckout(orderId);
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
paymentIntentClientSecret: clientSecret,
merchantDisplayName: 'Your Shop',
applePay: const PaymentSheetApplePay(merchantCountryCode: 'US'),
googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'US', testEnv: true),
),
);
try {
await Stripe.instance.presentPaymentSheet();
showProcessing(); // the webhook decides when the order is paid
} on StripeException catch (e) {
if (e.error.code == FailureCode.Canceled) return;
showError(e.error.localizedMessage ?? 'Payment failed');
}
}A successful presentPaymentSheet means the user finished the sheet. It isn't proof of payment for your records: some payment methods settle later, and the app can be closed before it reports back. Set testEnv: false for Google Pay once Google has approved production access.
Stripe webhooks: fulfil the order on the server
app.post("/stripe/webhook", { config: { rawBody: true } }, async (req, reply) => {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody!, req.headers["stripe-signature"] as string,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return reply.code(400).send();
}
if (await alreadyProcessed(event.id)) return reply.send();
if (event.type === "payment_intent.succeeded") {
const intent = event.data.object as Stripe.PaymentIntent;
await markOrderPaid(intent.metadata.orderId, intent.id);
}
await recordProcessed(event.id);
return reply.send();
});Stripe's docs add two rules: the same event can arrive more than once, so track event IDs, and events aren't guaranteed to arrive in order. When unsure, fetch the current object from the API. Also handle payment_intent.processing (show "pending") and payment_intent.payment_failed (offer another attempt).
Stripe Connect for marketplace apps
Stripe Connect lets a platform take payments and pay sellers. Two things trip people up.
Connect isn't escrow. Stripe's docs say: "Escrow has a precise legal definition, and Stripe doesn't provide escrow services or support escrow accounts." What Connect controls is where a charge lands and when a seller is paid out.
Standard, Express and Custom are now legacy account types. Stripe's account types page says new Connect platforms should use the Accounts v2 API, or v1 Accounts with controller properties, instead of choosing one of the three. If you've read "Stripe Connect vs Express" comparisons, that's the context: existing Express platforms keep working, but new builds describe the account's responsibilities instead.

| Charge type | Who the customer pays | Typical use |
|---|---|---|
| Direct charges | The connected account | SaaS platforms where sellers deal directly with their customers |
| Destination charges | Your platform, with the seller's share transferred automatically | One seller per order, like a booking or delivery app |
| Separate charges and transfers | Your platform, with transfers you create later | Orders split between sellers, or paying out after delivery |
A destination charge adds two fields to the PaymentIntent above:
application_fee_amount: 123, // your platform fee
transfer_data: { destination: sellerAccountId },With separate charges and transfers, you create the transfer when you're ready, linked to the original charge:
await stripe.transfers.create({
amount: 7000,
currency: "eur",
destination: sellerAccountId,
source_transaction: chargeId, // from the PaymentIntent's latest_charge
});Stripe integration checklist
- The secret key and webhook secret live only on the server. My post on hiding API keys in Flutter explains why build-time injection doesn't protect them.
- The server decides amount and currency.
- Every retryable POST to Stripe sends an idempotency key.
- Webhooks verify signatures, skip duplicate event IDs and tolerate out-of-order events.
- Card details only enter Stripe's UI, so they never touch your servers.
Testing a Stripe integration before going live
- Run
stripe listen --forward-to localhost:4242/stripe/webhookto send test events to your local server. - Pay with
4242 4242 4242 4242(succeeds),4000 0025 0000 3155(needs 3D Secure) and4000 0000 0000 9995(declined). - Close the app straight after paying and check the order still becomes paid through the webhook.
- Resend the same event and check nothing is fulfilled twice.
- For Connect, onboard a test connected account and check the transfer and payout appear.
- Switch the publishable key, secret key and webhook secret to live mode together, and register the live endpoint.
Selling subscriptions on iOS as well? The StoreKit subscriptions post shows how to merge several payment sources into one access record.
Frequently asked questions
How do I integrate Stripe in Flutter?
Add flutter_stripe, set the publishable key, create a PaymentIntent on your server, then call initPaymentSheet and presentPaymentSheet in the app. Fulfil the order on the payment_intent.succeeded webhook.
Why does PaymentSheet not open on Android?
Usually because MainActivity still extends FlutterActivity. flutter_stripe requires FlutterFragmentActivity and a Theme.AppCompat descendant, among the other setup steps in its README.
How do I add Apple Pay and Google Pay with flutter_stripe?
Set Stripe.merchantIdentifier for Apple Pay and pass PaymentSheetApplePay and PaymentSheetGooglePay with your merchant country code to SetupPaymentSheetParameters.
Can I use Stripe instead of Apple in-app purchase?
For physical goods and services used outside the app, yes, and guideline 3.1.3(e) requires a method other than in-app purchase. For digital features or content inside the app, guideline 3.1.1 requires in-app purchase.
Who confirms the PaymentIntent with PaymentSheet?
The app does, when the user pays. Your server creates the PaymentIntent and fulfils the order when the webhook arrives.
What is the difference between Stripe Connect and Express?
Express is one of Connect's account types. Stripe now calls Standard, Express and Custom legacy account types and points new platforms to the Accounts v2 API.
Does Stripe Connect hold funds in escrow?
No. Stripe's docs say it doesn't provide escrow services. You can delay payouts with manual payouts, within Stripe's time limits.
Why did my Stripe webhook signature check fail?
Usually because the framework parsed the JSON body before verification. Stripe needs the raw request body.
Sources
- App Review Guidelines (3.1.1 and 3.1.3) (Apple)
- Accept a payment (mobile Payment Element) (Stripe)
- The Payment Intents API (Stripe)
- flutter_stripe (pub.dev)
- Receive Stripe events in your webhook endpoint (Stripe)
- stripe-node README (webhook signing) (GitHub)
- Idempotent requests (Stripe)
- API keys (Stripe)
- Charges in a Connect integration (Stripe)
- Create destination charges (Stripe)
- Create separate charges and transfers (Stripe)
- Manual payouts (Stripe)
- Connected account types (Stripe)
- Testing (Stripe)
Keep reading
- Flutter In-App Purchase Guide: StoreKit 2, Play Billing and RevenueCatA Flutter in-app purchase guide for 2026: a subscription example, server verification on Firebase, testing, cancelling, and when RevenueCat is the better call.
- How to Hide API Keys in Flutter (and What to Do Instead)You can't hide an API key inside a Flutter app. Which keys are safe to ship, how to restrict them, and how to keep real secrets behind a proxy with App Check.
- StoreKit 2 Subscriptions in Flutter: Restore Purchases and Server NotificationsStoreKit 2 subscriptions in a Flutter app: server verification, App Store Server Notifications V2, restore purchases and one access record across gateways.
Comments
Share a question or note about this article.
Loading comments…