# Flutter SDK Reference

> For the complete documentation index, see [llms.txt](https://docs.banxa.com/llms.txt). Append `.md` to any page URL for its markdown version.


The Banxa Flutter SDK (`banxa_payments_flutter`) is the payment execution layer of Banxa Native for Flutter apps. It is the Dart counterpart to the [React Native SDK](/products/native-api/docs/sdk/sdk-reference): used alongside the Native API to present the native payment sheet for cards, Apple Pay, and Google Pay, the payment methods that cannot be handled through the API alone due to PCI requirements.

Bank transfers are handled directly by the Native API and do not involve the SDK. For the end-to-end integration walkthrough, see the [Integration Guide](/products/native-api/docs/guides/foundations).

## Which parts are relevant for Banxa Native

Your backend still owns orchestration
In a Banxa Native integration your backend handles pricing, identity, and eligibility through the Native API. The SDK's role is limited to payment execution: `BanxaPayments.startPayment` and the `BanxaPayments.checkoutEvents` stream.

The SDK also ships catalog, quote, and eligibility helpers that call partner-api v2 under `x-api-key`. Prefer the equivalent Native API endpoints for those, so that configuration, pricing, and eligibility stay on one HMAC-authenticated surface. The helpers are documented below for completeness.

## Requirements

| Requirement | Version |
|  --- | --- |
| Dart | `>=3.12.2 <4.0.0` |
| Flutter | `>=3.44.9` |
| iOS | 15.0 and above |
| Android | minSdk 24 and above |


The plugin ships iOS and Android implementations only.

You also need a Banxa partner account, which supplies the `apiKey` and `partnerId` used to configure the SDK.

## Installation

Add the dependency to `pubspec.yaml`:

```yaml
dependencies:
  banxa_payments_flutter: 0.1.0
```

Or from the command line:

```sh
flutter pub add banxa_payments_flutter:0.1.0
```

Pin the exact version
Pin `0.1.0`. Do not use a caret range: this is a preview release and Android native checkout is still on a Primer Checkout beta pin (`3.0.0-beta.2`; iOS is on `2.49.0`).

Do not depend on `banxa_payments_flutter_ios`, `banxa_payments_flutter_android`, or `banxa_payments_flutter_platform_interface` directly. Only the umbrella package is a supported dependency.

### iOS deployment target

Set the iOS deployment target to 15.0 in Xcode. After `flutter pub get`, build once with Flutter before opening the iOS project in Xcode:

```sh
flutter build ios --config-only
```

Skipping this can leave Xcode showing a 13.0 minimum, and plugin resolution fails.

Primer adds native binary size. Measure with `flutter build appbundle --analyze-size` and an iOS archive if binary size matters for your app.

### Permissions

The plugin merges `INTERNET` on Android. Hosted checkout and Primer may open the camera or photo library for KYC document capture, and those permissions must be declared in the **host app**.

| Platform | Declare |
|  --- | --- |
| iOS (`Info.plist`) | `NSCameraUsageDescription`, `NSPhotoLibraryUsageDescription`, and `NSMicrophoneUsageDescription` if you enable video KYC |
| Android (`AndroidManifest.xml`) | `android.permission.CAMERA` |


Missing iOS usage strings crash the app when capture starts.

The SDK is TLS only (`https`) and does not certificate-pin.

## Configuration

Call `configure` once per process, before any other SDK method. Re-calling it replaces the HTTP client and rebinds checkout listeners.

```dart
import 'package:banxa_payments_flutter/banxa_payments_flutter.dart';

await BanxaPayments.configure(
  const BanxaConfig(
    apiKey: 'YOUR_API_KEY',
    partnerId: 'your-partner-id',
    environment: BanxaEnvironment.sandbox,
  ),
);
```

### BanxaConfig

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `apiKey` | `String` | Yes | Your Banxa API key from the Partner Dashboard. |
| `partnerId` | `String` | Yes | Your partner identifier. |
| `environment` | `BanxaEnvironment` | No | Defaults to `BanxaEnvironment.sandbox`. |
| `applePayMerchantIdentifier` | `String?` | No | Apple Pay merchant identifier, for example `merchant.com.example`. Primer refuses Apple Pay without it. |
| `applePayMerchantName` | `String?` | No | Fallback name for the Apple Pay sheet, used only when the Primer client session does not carry a merchant name. |


`configure` also takes an optional `httpClient`:

```dart
await BanxaPayments.configure(config, httpClient: myClient);
```

All partner-api traffic is issued from Dart, so it appears in the DevTools network view and can be intercepted, logged, or mocked with your own `http.Client`.

### Environments

| `BanxaEnvironment` | Host |
|  --- | --- |
| `sandbox` | `https://api.banxa-sandbox.com` |
| `preprod` | `https://api.banxa-preprod.com` |
| `production` | `https://api.banxa.com` |


Requests go to `{host}/{partnerId}/v2` with `x-api-key` and `Content-Type: application/json`. Sandbox and production credentials are separate and are not interchangeable.

## Starting a payment

`startPayment` creates the order and launches checkout in one call. It returns a sealed `BanxaCheckoutLaunch` telling you which route was taken.

```dart
final launch = await BanxaPayments.startPayment(
  const CreateOrderRequest(
    orderType: OrderType.buy,
    crypto: 'ETH',
    fiat: 'EUR',
    fiatAmount: '40',
    walletAddress: '0x0000000000000000000000000000000000000000',
    email: 'user@example.com',
    redirectUrl: 'https://example.com/redirect',
    paymentMethodId: 'debit-credit-card',
    blockchain: 'ETH',
  ),
);

switch (launch) {
  case BanxaPrimerCheckoutLaunched():
    // Primer is on screen. Wait for checkoutEvents.
  case BanxaHostedCheckoutRequired():
    Navigator.of(context).push(
      MaterialPageRoute<void>(
        builder: (_) => Scaffold(
          appBar: AppBar(title: const Text('Checkout')),
          body: BanxaHostedCheckoutView(checkout: launch),
        ),
      ),
    );
}
```

### Launch results

| Type | Meaning |
|  --- | --- |
| `BanxaPrimerCheckoutLaunched` | The order returned a `nativeToken`, the payment method can run on this device, and Primer has been presented. Outcomes arrive on `checkoutEvents`. |
| `BanxaHostedCheckoutRequired` | No usable native route. Render `BanxaHostedCheckoutView` with this value. Carries `checkoutUrl` and the request's `redirectUrl`. |


Both types expose the created `order` as a `CreateOrderResponse`.

If the order has neither a usable native route nor a valid Banxa `https` checkout URL, `startPayment` throws `NativeCheckoutNotEligibleException`.

On iOS, Primer presents a single payment method (`showPaymentMethod`). On Android, Primer Checkout 3.x presents the methods from the client session; the requested Banxa method is still recorded natively.

Gate on eligibility before you call startPayment
`startPayment` does not call any eligibility endpoint. Call `POST /eapi/v0/eligibility` from your backend and confirm `paymentReady: true` before invoking the SDK, so you can drive the KYC remediation loop from the `requirements[]` array.

See [Interpreting Eligibility](/products/native-api/docs/how-it-works/interpreting-eligibility).

The SDK does not accept a `quoteId`. Use indicative pricing from `GET /eapi/v0/price` and re-price close to the point of payment to reduce rate drift.

### Creating an order without presenting checkout

```dart
final order = await BanxaPayments.createOrder(request);
```

`createOrder` posts to `/buy` or `/sell` and returns the `CreateOrderResponse` without presenting any checkout UI.

Do not create orders in advance
Banxa checkout URLs must be loaded within about one minute of order creation. Prefer `startPayment`, which creates and presents in a single call. If you use `createOrder`, present checkout immediately.

## Checkout events

Subscribe to `BanxaPayments.checkoutEvents` before calling `startPayment`. The stream carries outcomes from both native Primer checkout and `BanxaHostedCheckoutView`.

```dart
BanxaPayments.checkoutEvents.listen((event) {
  switch (event) {
    case BanxaCheckoutCompleted(:final paymentId, :final orderId, :final status):
      // Success.
    case BanxaCheckoutFailed(:final message):
      // Error.
    case BanxaCheckoutDismissed():
      // Sheet closed.
  }
});
```

| Event | Fires when | Fields |
|  --- | --- | --- |
| `BanxaCheckoutCompleted` | The customer completed payment. | `paymentId`, `orderId`, `status`, all `String?` |
| `BanxaCheckoutFailed` | Any checkout failure. | `message`, a short reason, never a URL |
| `BanxaCheckoutDismissed` | The customer closed checkout, or the hosted WebView was disposed before a terminal URL. | none |


`status` may be null on Android. Primer may emit `dismissed` after `completed` or `failed`, so treat the events as distinct rather than mutually exclusive.

Completion identifiers are read from Primer or from hosted-checkout query parameters (`paymentId`, `orderId`, `status`, plus `payment_id` and `order_id` aliases). Raw URLs are never surfaced to your app.

Checkout events are a UI signal, not the authoritative order state. Confirm the final state from [webhooks](/products/native-api/docs/transaction-lifecycle/webhooks) before you credit a customer.

## Hosted checkout fallback

When `startPayment` returns `BanxaHostedCheckoutRequired`, render `BanxaHostedCheckoutView`:

```dart
BanxaHostedCheckoutView(checkout: launch)
```

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `checkout` | `BanxaHostedCheckoutRequired` | Yes | The launch result from `startPayment`. |


Behaviour to design around:

- The view reports on `checkoutEvents` and never pops itself. Dismiss it yourself when a terminal event arrives.
- Disposing it before a terminal URL emits `BanxaCheckoutDismissed`.
- A first page that has not loaded within 30 seconds emits `BanxaCheckoutFailed`.
- The first URL must be `https` on a Banxa-owned host: `banxa.com`, `banxa-sandbox.com`, or `banxa-preprod.com`, including subdomains. Later navigations may leave Banxa for bank or wallet pages during 3DS, but must stay `https`.
- JavaScript is enabled for the hosted UI. There is no JavaScript bridge into your app.


## Apple Pay and Google Pay

Apple Pay requires a merchant identifier and the matching `com.apple.developer.in-app-payments` entitlement, added in Xcode under **Signing & Capabilities, Apple Pay**.

```dart
await BanxaPayments.configure(
  const BanxaConfig(
    apiKey: 'YOUR_API_KEY',
    partnerId: 'your-partner-id',
    applePayMerchantIdentifier: 'merchant.com.yourcompany.yourapp',
    applePayMerchantName: 'Your Store',
  ),
);
```

`startPayment` returns `BanxaHostedCheckoutRequired` instead of presenting Primer when:

- No `applePayMerchantIdentifier` is configured.
- The app is running on the iOS simulator. Apple Pay does not run on the simulator.
- The device has no usable card in Wallet.
- The request is for Apple Pay on Android.
- Google Play services are missing, for Google Pay.


See [Apple Pay](/products/native-api/docs/guides/apple-pay) and [Google Pay](/products/native-api/docs/guides/google-pay) for platform setup.

## Models

### CreateOrderRequest

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `orderType` | `OrderType` | Yes | `OrderType.buy` or `OrderType.sell`. Selects `POST /buy` or `POST /sell`. |
| `crypto` | `String` | Yes | Crypto asset symbol, for example `ETH`. |
| `fiat` | `String` | Yes | Fiat currency code, for example `EUR`. |
| `fiatAmount` | `String` | Yes | Fiat amount as a string. |
| `walletAddress` | `String` | Yes | Destination wallet address. |
| `email` | `String` | Yes | Customer email address. |
| `redirectUrl` | `String` | Yes | URL the customer returns to after hosted checkout. |
| `id` | `String?` | No | Your order id. |
| `paymentMethodId` | `String?` | No | Banxa payment method slug, for example `apple-pay`. Required in practice: native checkout is skipped without it. |
| `blockchain` | `String?` | No | Explicit blockchain network. |
| `cryptoAmount` | `String?` | No | Crypto amount when ordering by crypto value. |
| `walletAddressTag` | `String?` | No | Tag or memo for chains that require it. |
| `subPartnerId` | `String?` | No | Sub-partner identifier. |
| `metadata` | `String?` | No | Opaque metadata string. |
| `externalCustomerId` | `String?` | No | Your customer identifier. Pass the same value you use as `identityReference`. |
| `externalOrderId` | `String?` | No | Your order reference. |
| `discountCode` | `String?` | No | Promotion or discount code. |


`crypto`, `fiat`, `fiatAmount`, `walletAddress`, `email`, and `redirectUrl` are validated locally before the request is sent. A blank value throws `ValidationException` with a `FieldError` per missing field.

A missing memo can permanently lose funds
XRP, XLM, EOS, and ATOM require a memo or tag. Pass it as `walletAddressTag`. An on-ramp to one of these chains without the memo can be unrecoverable.

### CreateOrderResponse

| Field | Type | Notes |
|  --- | --- | --- |
| `id` | `String` | Banxa order id. |
| `checkoutUrl` | `String?` | Hosted checkout URL. Drives the `BanxaHostedCheckoutRequired` route. |
| `nativeToken` | `String?` | Primer client token. Present when native checkout is available. |
| `status` | `String?` | Order status at creation. |
| `paymentMethodId` | `String?` | Catalog slug. Numeric wire values are stringified. |
| `paymentMethodType` | `String?` | Payment method type. |
| `orderType` | `String?` | `buy` or `sell`. |
| `fiat`, `fiatAmount`, `crypto`, `cryptoAmount`, `blockchain` | `String?` | Echoed order amounts and assets. |
| `walletAddress`, `walletAddressTag` | `String?` | Echoed destination. |
| `externalCustomerId`, `externalOrderId` | `String?` | Echoed partner references. |
| `createdAt`, `updatedAt` | `String?` | Timestamps. |


## Catalog and quote helpers

These call partner-api v2 under `x-api-key` and require a configured SDK. In a Banxa Native integration, prefer the equivalent Native API endpoints so that configuration and pricing stay on the HMAC-authenticated surface.

```dart
final countries = await BanxaPayments.fetchCountries();
final fiats = await BanxaPayments.fetchFiats(orderType: OrderType.buy);
final cryptos = await BanxaPayments.fetchCrypto(orderType: OrderType.buy);
final methods = await BanxaPayments.fetchPaymentMethods(
  orderType: OrderType.buy,
  fiat: 'USD',
);
```

| Method | Endpoint | Returns |
|  --- | --- | --- |
| `fetchCountries()` | `GET /countries` | `List<Country>` |
| `fetchFiats({required orderType})` | `GET /fiats/{buy|sell}` | `List<Fiat>` |
| `fetchCrypto({required orderType})` | `GET /crypto/{buy|sell}` | `List<Crypto>` |
| `fetchPaymentMethods({required orderType, fiat})` | `GET /payment-methods/{buy|sell}` | `List<PaymentMethod>` |
| `fetchQuotes({required orderType, required request})` | `GET /quotes/{buy|sell}` | `List<Quote>` |


### Catalog types

| Type | Fields |
|  --- | --- |
| `Country` | `id`, `description`, `states: List<CountryState>` |
| `CountryState` | `id`, `description` |
| `Fiat` | `id`, `description?`, `symbol?`, `supportedPaymentMethods: List<FiatPaymentMethod>` |
| `FiatPaymentMethod` | `id`, `name?`, `minimum?`, `maximum?` |
| `Crypto` | `id`, `description?`, `blockchains: List<Blockchain>` |
| `Blockchain` | `id`, `description?`, `isDefaultBlockchain?`, `address?`, `network?`, `minimum?`, `unsupportedCountries: Map<String, List<String>>` |
| `PaymentMethod` | `id`, `name?`, `description?`, `supportedFiats: List<String>` |


In `unsupportedCountries`, the key is a country code and the value is the restricted state codes. An empty list means the whole country is restricted.

### Quotes

```dart
final quotes = await BanxaPayments.fetchQuotes(
  orderType: OrderType.buy,
  request: const QuoteRequest(
    paymentMethodId: 'debit-credit-card',
    crypto: 'ETH',
    blockchain: 'ETH',
    fiat: 'AUD',
    fiatAmount: '200',
  ),
);
```

| `QuoteRequest` field | Type | Required | Notes |
|  --- | --- | --- | --- |
| `paymentMethodId` | `String` | Yes | Payment method slug. |
| `crypto` | `String` | Yes | Crypto asset symbol. |
| `blockchain` | `String` | Yes | Blockchain network. |
| `fiat` | `String` | Yes | Fiat currency code. |
| `fiatAmount` | `String?` | Conditional | Provide this or `cryptoAmount`. |
| `cryptoAmount` | `String?` | Conditional | Provide this or `fiatAmount`. |
| `externalCustomerId` | `String?` | No | Your customer identifier. |
| `ipAddress` | `String?` | No | Customer IP for regional pricing. |
| `discountCode` | `String?` | No | Promotion or discount code. |


| `Quote` field | Type | Notes |
|  --- | --- | --- |
| `paymentMethodId` | `String?` | Payment method the quote applies to. |
| `cryptoAmount` | `String?` | Crypto the customer receives. |
| `fiatAmount` | `String?` | Fiat the customer spends. |
| `processingFee` | `String?` | Banxa processing fee in fiat. |
| `networkFee` | `String?` | Blockchain network fee in fiat. |
| `discount` | `QuoteDiscount?` | Present when a discount code was applied. Carries `discountCode` and `originalQuote: QuoteOriginalAmounts?` with `originalFiatAmount`, `originalCryptoAmount`, `originalProcessingFee`, `originalNetworkFee`. |


`fetchQuotes` returns a list because Banxa returns multiple entries when discount codes apply. Quotes carry no quote id and are not accepted by `startPayment`. Do not cache them: call immediately before showing a price.

## Eligibility helper

```dart
final eligibility = await BanxaPayments.checkEligibility(request);
if (eligibility.paymentReady == true) {
  await BanxaPayments.startPayment(request);
}
```

`checkEligibility` posts the same body as `createOrder` to partner-api `POST /eligibility` and returns:

| `EligibilityResponse` field | Type | Notes |
|  --- | --- | --- |
| `id` | `String?` | Eligibility check identifier. |
| `paymentReady` | `bool?` | Whether the customer can pay now. |
| `kycRequirements` | `List<String>` | Outstanding KYC requirement identifiers. |
| `message` | `String?` | Human-readable detail. |


This is opt-in, and `startPayment` never calls it. For a Banxa Native integration, use `POST /eapi/v0/eligibility` from your backend instead: it returns the full `requirements[]` dictionary you need to drive remediation, on the HMAC-authenticated surface. See [Interpreting Eligibility](/products/native-api/docs/how-it-works/interpreting-eligibility).

## Primer session updates

```dart
await BanxaPayments.updatePrimerSession(
  primerToken: 'PRIMER_CLIENT_TOKEN',
  savedCard: false,
);
```

`updatePrimerSession` posts to `/primer/session` for mid-flow session patches, and accepts an optional `additionalFields` map of `String` values. Most integrations do not need it.

## Error handling

Every SDK failure is a subclass of the sealed `BanxaPaymentsException`, which exposes a `message`. Catch the sealed type and switch on the subclass.

| Exception | Cause |
|  --- | --- |
| `SdkNotConfiguredException` | A method was called before `BanxaPayments.configure`. |
| `MissingCredentialsException` | `apiKey` or `partnerId` was blank. Exposes `fields`. |
| `UnauthorizedException` | HTTP 401. Check the key matches the environment. |
| `ValidationException` | HTTP 400 or 422, or local field validation. Exposes `errors: List<FieldError>`, each with `field` and `messages`. |
| `ServerException` | Any other non-success HTTP status. Exposes `statusCode`. |
| `NetworkException` | Transport failure, including TLS errors and timeouts. |
| `CheckoutFailedException` | Native Primer presentation failed. |
| `NativeCheckoutNotEligibleException` | The order has no usable native Primer path and no valid hosted `checkoutUrl`. |
| `UnknownException` | Unexpected local failure, for example a JSON decode error. |


```dart
try {
  await BanxaPayments.startPayment(request);
} on ValidationException catch (e) {
  for (final error in e.errors) {
    debugPrint('${error.field}: ${error.messages.join(', ')}');
  }
} on BanxaPaymentsException catch (e) {
  debugPrint(e.message);
}
```

Show user-facing messages only from validated error fields. Do not expose raw error strings that may include internal detail. See [Error Codes](/products/native-api/docs/getting-started/error-codes).

## Not available in the SDK

| Capability | Use instead |
|  --- | --- |
| Identity creation, KYC sharing, document sharing | Native API. See [Identity and KYC](/products/native-api/docs/how-it-works/identity-kyc) |
| The full eligibility `requirements[]` dictionary | `POST /eapi/v0/eligibility` from your backend |
| Locked pricing (`quoteId`) | Bank transfer ramps only. See [Quotes and Pricing](/products/native-api/docs/how-it-works/quotes-and-pricing) |
| Order lookup and reconciliation | [Webhooks](/products/native-api/docs/transaction-lifecycle/webhooks) and the Native API |


## Google Pay approval

When your Android app presents Google Pay, Google requires app-level approval before you can process live payments. Banxa's own approval does not cover payments presented inside your app. Start the approval process early: it is the most common cause of a delayed Android launch. See the [Google Pay guide](/products/native-api/docs/guides/google-pay).

## Payment methods and supported fiats

Availability varies by partner, region, and order type. Treat `fetchPaymentMethods` or the Native API configuration endpoints as the source of truth. The table below is a static reference.

| Payment method id | Supported fiats |
|  --- | --- |
| `debit-credit-card` | `AED`, `ARS`, `AUD`, `CAD`, `CHF`, `CZK`, `DKK`, `EUR`, `GBP`, `HKD`, `IDR`, `INR`, `JPY`, `KRW`, `MXN`, `MYR`, `NGN`, `NOK`, `NZD`, `PHP`, `PLN`, `QAR`, `RUB`, `SAR`, `SEK`, `SGD`, `THB`, `TRY`, `TWD`, `USD`, `VND`, `ZAR` |
| `google-pay` | `AUD`, `EUR`, `USD` |
| `apple-pay` | `AUD`, `EUR`, `GBP`, `USD` |
| `payid-bank-transfer` | `AUD` |
| `zar-bank-transfer` | `ZAR` |
| `pse` | `COP` |
| `khipu` | `CLP` |
| `interac-bank-transfer` | `CAD` |
| `klarna-paynow` | `EUR` |
| `ideal-bank-transfer` | `AUD`, `EUR` |
| `sepa-bank-transfer` | `EUR` |
| `gbp-bank-transfer` | `GBP` |
| `spei` | `MXN` |


## Next steps

- [Integration Guide](/products/native-api/docs/guides/foundations): end-to-end walkthrough.
- [Apple Pay](/products/native-api/docs/guides/apple-pay): merchant identifier and entitlement setup.
- [Google Pay](/products/native-api/docs/guides/google-pay): app approval and platform setup.
- [Cards](/products/native-api/docs/guides/cards): 3D Secure setup.
- [Interpreting Eligibility](/products/native-api/docs/how-it-works/interpreting-eligibility): the requirements dictionary and remediation loop.