Dodo Payments Checkout Flutter SDK

Open Dodo Payments' hosted checkout in a system browser tab (SFSafariViewController on iOS, a Custom Tab on Android) and get a clean result from one call.

This package is a thin wrapper: all checkout logic lives in the native iOS (DodoCheckout, Swift) and Android (com.dodopayments.api:checkout-android) cores. The Dart layer passes the call through a typed Pigeon channel and maps the result back.

Install

dependencies:
  dodopayments_checkout: ^1.0.0

Requirements: Flutter 3.44+ / Dart 3.12+, iOS 16+, Android minSdk 23.

Setup

Register a callback URL scheme so the OS routes the checkout return back to your app:

  • iOS: add a URL type for your scheme in Info.plist, and forward incoming URLs (e.g. via app_links) into DodoCheckout.instance.handleOpenURL(url)SFSafariViewController has no in-process way to catch its own return URL.

  • Android: set your callback scheme as a Gradle manifest placeholder — the underlying Android core's own manifest already declares the redirect activity's intent-filter, so this one property is the entire setup cost:

    android {
        defaultConfig {
            manifestPlaceholders["dodoCallbackScheme"] = "myapp"
        }
    }
    

    If your MainActivity sets android:taskAffinity="" (the stock flutter create template default, meant to stop the launcher icon from resurrecting a stale engine instance), remove it or give the SDK's activities the same explicit affinity. The SDK's own activities inherit the app's default task affinity, and leaving MainActivity on a different one is a real mismatch — some OEM Android skins (observed on Vivo) can then treat a long-lived Custom Tab's return as belonging to a different task and recreate the SDK's host activity from scratch, losing the in-flight checkout and surfacing PLATFORM_ERROR: Checkout was launched without its parameters.

Use

import 'package:dodopayments_checkout/dodopayments_checkout.dart';

final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl), // from your backend's POST /checkouts
    returnUrl: Uri.parse('myapp://checkout/return'), // scheme must be registered (see Setup)
    onEvent: (event) => print(event.type), // logging only — never decide outcome from events
  ),
);

switch (result.status) {
  case CheckoutStatus.succeeded: showSuccess(result.paymentId); // UI only — confirm server-side
  case CheckoutStatus.failed:    showFailure();
  case CheckoutStatus.cancelled: await reconcileAbandonedSession(); // outcome unknown — NOT a failure
  case CheckoutStatus.pending:   await reconcileAbandonedSession(); // may be unparsed, not just async
  case CheckoutStatus.expired:   showExpired();
}

Your backend creates the checkout session (with your secret key) and sends the checkout_url to the app. Set the session's return_url to the same URL you pass as returnUrl — it never has to resolve, because the SDK cancels the navigation before it loads.

What the result means

The result comes from the return_url query string. It is a UI hint, not proof of payment. This SDK never calls the Dodo API and holds no API key. Grant access on your backend from the webhook (payment.succeeded / subscription.active) or by retrieving the payment with your secret key. result.raw carries every query parameter verbatim.

Verify the payment

Confirm every payment from your backend, not from the mobile result:

  • Webhook: Dodo Payments calls your backend when a payment succeeds or a subscription activates. Check the Webhooks guide.
  • Verification API: look up paymentId with your secret key via Get Payment Detail.

cancelled is not a failure

CheckoutStatus.cancelled means the user dismissed the browser before any return URL arrived, so the SDK never learned the outcome. The payment may have gone through. A user who pays and then taps ✕ while the hosted "Payment Successful" page counts down its redirect produces cancelled, and is indistinguishable, from the SDK's side, from a user who closed the browser without paying.

Showing "Payment failed" here tells a paying customer their money vanished. Resolve it instead: the SDK keeps the session on record for exactly this case.

Abandoned sessions

A session stays on record whenever the SDK never saw a return URL it could resolve to a durable outcome — the app was killed mid-checkout, start completed with CheckoutStatus.cancelled, or it completed with CheckoutStatus.pending from an unparseable return URL rather than a genuinely async payment method. Reconcile it server-side, both on next launch and right after a cancelled or pending result:

import 'package:dodopayments_checkout/dodopayments_checkout.dart';

Future<void> reconcileAbandonedSession() async {
  final abandoned = await DodoCheckout.instance.getAbandonedSession();
  if (abandoned == null) {
    dismiss(); // nothing in flight
    return;
  }
  // Ask *your* backend what happened to abandoned.sessionId — it has the
  // webhook (`payment.succeeded`) or can call Get Payment Detail with your
  // secret key. Show a spinner while you wait; an async method may still be
  // settling, so treat "no record yet" as pending, not failed — and only
  // clear the record once you have a terminal outcome, or a later retry
  // has nothing left to reconcile against if this one comes back.
  final outcome = await myBackend.outcomeForSession(abandoned.sessionId);
  if (outcome.isTerminal) {
    await DodoCheckout.instance.clearAbandonedSession();
  }
  show(outcome);
}

Errors

start throws CheckoutException only for misuse or platform failure. The code is a CheckoutErrorCode:

Code Native code Meaning
invalidCheckoutUrl INVALID_CHECKOUT_URL Not a checkout.dodopayments.com / test.checkout.dodopayments.com session URL
invalidReturnUrl INVALID_RETURN_URL Not a valid absolute URL
alreadyInProgress ALREADY_IN_PROGRESS A checkout is already running (only one at a time)
platformError PLATFORM_ERROR Unexpected platform failure

A user cancelling or a declined payment is a result (CheckoutStatus.cancelled / CheckoutStatus.failed), never an exception.

Example

example/lib/main.dart demonstrates a full price-page flow. Generate its platform folders once with flutter create --platforms=android,ios . inside example/ and run it.

Development notes

  • Platform channels are generated by Pigeon from pigeons/messages.dart. After editing that file: dart run pigeon --input pigeons/messages.dart.
  • iOS bundles the Swift core as source (see ios/dodopayments_checkout.podspec); Android depends on the com.dodopayments.api:checkout-android Maven artifact (see android/build.gradle for local-dev pointers to the sibling cores in this monorepo).

Libraries

dodopayments_checkout
Dodo Payments mobile checkout for Flutter.