> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flutter

> Open Dodo Payments hosted checkout from a Flutter app in a system browser view with the official Flutter package, and get a typed checkout result back from one call.

<Info>
  This page covers the official Dodo Payments Flutter package, `dodopayments_checkout` on pub.dev. A separate, community-built package also exists. See
  [Community Projects](/community/projects).
</Info>

<CardGroup cols={2}>
  <Card title="Checkout Sessions API" icon="cart-shopping" href="/developer-resources/checkout-session">
    Create the `checkout_url` that this SDK opens, from your backend.
  </Card>

  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    See how this SDK fits into the full mobile payment flow.
  </Card>
</CardGroup>

`dodopayments_checkout` opens Dodo Payments hosted checkout in `SFSafariViewController` on iOS and in a Custom Tab on Android, and returns a typed `CheckoutResult`. It uses the same native code as the standalone [iOS](/developer-resources/sdks/ios) and
[Android](/developer-resources/sdks/android) SDKs, and all checkout logic lives in that native code. The Dart layer passes each call through a typed
[Pigeon](https://pub.dev/packages/pigeon) channel. The package holds no API key and never calls the Dodo Payments API.

**Requirements:** Flutter 3.44 or later with Dart 3.12 or later, iOS 16 or later, and Android `minSdk` 23.

## Installation

<Steps>
  <Step title="Add the Dependency">
    Add the package to `pubspec.yaml`:

    ```yaml pubspec.yaml theme={null}
    dependencies:
      dodopayments_checkout: ^1.1.0
    ```

    [Appearance customization](#appearance-customization) requires version 1.1.0 or later.

    The Android plugin compiles against Android SDK 35 by default. If another plugin needs a higher `compileSdk`, set `dodoCompileSdk` in your app's `gradle.properties`.
  </Step>

  <Step title="Register a Callback URL Scheme">
    Register a URL scheme so that the operating system routes the checkout's return URL back to your app. Use this scheme in the `returnUrl` you pass to the SDK, and set the same URL as the checkout session's `return_url` when your backend creates the session. The URL doesn't need to load a real page.

    <Tabs>
      <Tab title="iOS">
        Add a URL type for your scheme in `ios/Runner/Info.plist`:

        ```xml ios/Runner/Info.plist theme={null}
        <key>CFBundleURLTypes</key>
        <array>
          <dict>
            <key>CFBundleURLName</key>
            <string>myapp</string>
            <key>CFBundleURLSchemes</key>
            <array>
              <string>myapp</string>
            </array>
          </dict>
        </array>
        ```

        `SFSafariViewController` can't catch its own return URL, so iOS opens the URL in your app instead. Forward every incoming URL to the SDK, for example from
        [`app_links`](https://pub.dev/packages/app_links):

        ```dart theme={null}
        import 'package:dodopayments_checkout/dodopayments_checkout.dart';

        // url is the incoming link as a String. For a Uri from app_links, pass uri.toString().
        DodoCheckout.instance.handleOpenURL(url);
        ```

        <Note>
          You can forward every URL. `handleOpenURL` acts only on a URL that matches the
          `returnUrl` of the checkout in progress, and resolves `true` for it. For any
          other URL, it resolves `false`. On Android, it always resolves `false`.
        </Note>
      </Tab>

      <Tab title="Android">
        Set your callback scheme as a Gradle manifest placeholder in `android/app/build.gradle.kts`. The same line works in a Groovy `build.gradle` file:

        ```kotlin android/app/build.gradle.kts theme={null}
        android {
            defaultConfig {
                manifestPlaceholders["dodoCallbackScheme"] = "myapp"
            }
        }
        ```

        <Warning>
          If `MainActivity` sets `android:taskAffinity=""` (the default in the
          `flutter create` template), remove it or give the SDK's activities the same
          affinity. Otherwise, some OEM Android builds can lose the checkout in progress
          and fail with `PLATFORM_ERROR` and the message "Checkout was launched without its parameters."
        </Warning>
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Usage

Call `DodoCheckout.instance.start` with the `checkout_url` from your backend:

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';

// showSuccess(), showFailure(), showExpired(), and reconcileAbandonedSession()
// are your own functions. See Abandoned Sessions for what to reconcile.
final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl), // from your backend's checkout session
    returnUrl: Uri.parse('myapp://checkout/return'), // scheme must be registered (see Installation)
    onEvent: (event) => print(event.type), // logging only
  ),
);

switch (result.status) {
  case CheckoutStatus.succeeded: showSuccess(result.paymentId);
  case CheckoutStatus.failed:    showFailure();
  case CheckoutStatus.cancelled: await reconcileAbandonedSession(); // outcome unknown, not a failure
  case CheckoutStatus.pending:   await reconcileAbandonedSession();
  case CheckoutStatus.expired:   showExpired();
}
```

`onEvent` receives events whose `type` is `CheckoutEventType.opened`, `returnReceived`, or `closed`. Use them for logging only, never to decide the outcome.

## What the Result Means

The SDK builds `CheckoutResult` from the query parameters on the return URL.

<Warning>
  `result.status` is a UI hint, not proof of payment. Confirm every payment
  from your backend, with the `payment.succeeded` or `subscription.active`
  webhook.
</Warning>

<ParamField body="status" type="CheckoutStatus" required>
  One of five values:

  * `succeeded`: the return URL has `status=succeeded` (one-time payment) or `status=active` (subscription).
  * `failed`: the payment was declined (`status=failed`).
  * `cancelled`: the customer closed the browser view before the return URL arrived. The SDK doesn't know the outcome, and the payment may have succeeded, so don't show a failure screen. Reconcile the [abandoned session](#abandoned-sessions) instead.
  * `pending`: the payment settles later (`status=processing` or any `requires_*` value), or the `status` parameter was missing or unrecognized. Reconcile it like `cancelled`.
  * `expired`: the checkout session expired (`status=expired`).
</ParamField>

<ParamField body="paymentId" type="String?">
  The `payment_id` query parameter, when the return URL includes one. Show it in your UI, but don't use it
  to grant access. See [Verify the Payment](#verify-the-payment).
</ParamField>

<ParamField body="subscriptionId" type="String?">
  The `subscription_id` query parameter. Set for subscription checkouts.
</ParamField>

<ParamField body="licenseKeys" type="List<String>?">
  The `license_key` query parameter. Set when the checkout includes license key products.
</ParamField>

<ParamField body="customerEmail" type="String?">
  The `email` query parameter. Set when checkout captures an email address.
</ParamField>

<ParamField body="raw" type="Map<String, String>">
  Every query parameter from the return URL, verbatim.
</ParamField>

## Verify the Payment

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/developer-resources/webhooks">
    Dodo Payments calls your backend when a payment succeeds or a subscription activates.
  </Card>

  <Card title="Get Payment Detail" icon="magnifying-glass" href="/api-reference/payments/get-payments-1">
    Look up `paymentId` with your secret key to check its status.
  </Card>
</CardGroup>

Grant access only after one of these confirms the payment. Don't rely on
`result.status` alone.

## Appearance Customization

To change the checkout browser's toolbar, buttons, and color scheme, pass a `BrowserCustomization` as `customization` on `CheckoutParams`. Android Custom Tabs and iOS `SFSafariViewController` expose different native controls, so the options are split into `AndroidBrowserOptions` and `IosBrowserOptions`. Each platform ignores the other's options. Every field is optional and defaults to `null`. For a `null` field, the SDK doesn't set that option and the platform applies its own default.

<AccordionGroup>
  <Accordion title="Android — Custom Tab">
    <ParamField body="toolbarColor" type="Color?">
      Toolbar background color.
    </ParamField>

    <ParamField body="navigationBarColor" type="Color?">
      Navigation bar color.
    </ParamField>

    <ParamField body="navigationBarDividerColor" type="Color?">
      Color of the divider above the navigation bar.
    </ParamField>

    <ParamField body="closeButtonStyle" type="CloseButtonStyle?">
      `standard` shows the system "X" icon. `back` shows a back arrow that the SDK draws.
    </ParamField>

    <ParamField body="closeButtonPosition" type="CloseButtonPosition?">
      The side of the toolbar where the close button appears: `start` or `end`.
    </ParamField>

    <ParamField body="shareButtonEnabled" type="bool?">
      Shows the toolbar's share icon. `false` hides it.
    </ParamField>

    <ParamField body="showTitleEnabled" type="bool?">
      Shows the page title under the URL in the toolbar.
    </ParamField>

    <ParamField body="urlBarHidingEnabled" type="bool?">
      Hides the toolbar automatically as the page scrolls.
    </ParamField>

    <ParamField body="bookmarksButtonEnabled" type="bool?">
      Shows "Bookmark this page" in the overflow menu.
    </ParamField>

    <ParamField body="downloadsButtonEnabled" type="bool?">
      Shows "Download page" in the overflow menu.
    </ParamField>

    <ParamField body="colorScheme" type="BrowserColorScheme?">
      `light` or `dark` forces that appearance regardless of the device's system setting. `system` follows the system setting.
    </ParamField>
  </Accordion>

  <Accordion title="iOS — SFSafariViewController">
    <ParamField body="dismissButtonStyle" type="DismissButtonStyle?">
      Style of the dismiss button: `done`, `close`, or `cancel`. iOS decides whether it renders as a label or an icon.
    </ParamField>

    <ParamField body="presentationStyle" type="PresentationStyle?">
      `pageSheet` (used when you leave this `null`) presents a card that the customer can swipe down to dismiss. `fullScreen` covers the whole screen.
    </ParamField>

    <ParamField body="barCollapsingEnabled" type="bool?">
      Lets the toolbar collapse as the page scrolls. It has a visible effect only when `presentationStyle` is `fullScreen`. With `pageSheet`, the bars stay pinned regardless of this setting.
    </ParamField>

    <ParamField body="colorScheme" type="BrowserColorScheme?">
      `light` or `dark` forces that appearance regardless of the device's system setting. `system` follows the system setting.
    </ParamField>
  </Accordion>
</AccordionGroup>

iOS has no toolbar color option, because the underlying `SFSafariViewController` tint properties are deprecated as of iOS 26.

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';
import 'package:flutter/material.dart' show Color;

final result = await DodoCheckout.instance.start(
  CheckoutParams(
    checkoutUrl: Uri.parse(checkoutUrl),
    returnUrl: Uri.parse('myapp://checkout/return'),
    customization: BrowserCustomization(
      android: AndroidBrowserOptions(
        toolbarColor: Color(0xFF6366F1),
        closeButtonStyle: CloseButtonStyle.back,
      ),
      ios: IosBrowserOptions(
        presentationStyle: PresentationStyle.fullScreen,
        colorScheme: BrowserColorScheme.dark,
      ),
    ),
  ),
);
```

## Errors

`start` throws `CheckoutException` only for misuse or a platform failure. Read the reason from `code`, a `CheckoutErrorCode`. The native code string is in `nativeCode`.
A customer who cancels, or a declined payment, is always a result, never an exception.

* `invalidCheckoutUrl` (`INVALID_CHECKOUT_URL`): `checkoutUrl` isn't an `https` checkout session URL (path starting with `/session/`) on `checkout.dodopayments.com` or `test.checkout.dodopayments.com`.
* `invalidReturnUrl` (`INVALID_RETURN_URL`): `returnUrl` isn't an absolute URL with a scheme and a host.
* `alreadyInProgress` (`ALREADY_IN_PROGRESS`): another checkout is running. Only one checkout can run at a time.
* `platformError` (`PLATFORM_ERROR`): an unexpected platform failure. Unknown native errors also map to this code.

## Abandoned Sessions

<Info>
  The native SDK records the checkout session when checkout starts, and clears the
  record only when checkout ends with `succeeded`, `failed`, or `expired`. The record
  stays when the app is killed during checkout, and after a `cancelled` or `pending`
  result. Check for it on the next launch and after every `cancelled` or `pending` result.
</Info>

```dart theme={null}
import 'package:dodopayments_checkout/dodopayments_checkout.dart';

final abandoned = await DodoCheckout.instance.getAbandonedSession();
if (abandoned != null) {
  // Ask your backend for the outcome of abandoned.sessionId, and show it.
  // Clear the record only after the outcome is final:
  await DodoCheckout.instance.clearAbandonedSession();
}
```

`abandoned.sessionId` is the checkout session ID, which starts with `cks_`. `abandoned.createdAt` is the `DateTime` checkout started. Your backend can look up the session with [Get Checkout Session](/api-reference/checkout-sessions/get-checkouts), which returns its `payment_id` and `payment_status`. Until the payment reaches a final status, treat it as pending, not failed.

## Related

<CardGroup cols={2}>
  <Card title="Mobile Integration Guide" icon="mobile" href="/developer-resources/mobile-integration">
    The same contract for Android, iOS, and React Native.
  </Card>

  <Card title="Community Projects" icon="users" href="/community/projects">
    A separate, community-built Flutter package also exists.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.