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

# Flow payments with Flutter

> End-to-end guide for completing a Flow payment with the Flutter SDK.

> [!NOTE]
>
> This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.

Flow payments let your users pay from a supported wallet, exchange, or deposit address, and the funds settle in the token and chain you configure. This guide covers wallet and deposit-address payments with the Dynamic Flutter SDK. [Payment lifecycle and states](https://docs.dynamic.xyz/docs/flow/lifecycle-walkthrough.md) explains how a payment moves from creation to settlement.

## Prerequisites

Before starting a flow on the client, create it from your backend. The flow creation endpoint is server-side only, authenticated by an API token with `flow.write` scope: this is where the amount, currency, settlement, and destination are fixed. Set `currency` to `"USD"` for a fiat amount, or `"SETTLEMENT_TOKEN"` to denominate the amount in the settlement token's units: cryptocurrency is accepted only for payment flows whose settlement config lists exactly one token. [Amounts you will see](https://docs.dynamic.xyz/docs/flow/lifecycle-walkthrough.md#amounts-you-will-see) explains how the requested, quoted, and delivered amounts differ.

```bash theme={"system"}
# Create a flow from your backend (returns a flowId)
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/server/$DYNAMIC_ENVIRONMENT_ID/flow/payment" \
  --header "Authorization: Bearer $DYNAMIC_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": "25.00",
    "currency": "USD",
    "settlementConfig": {
      "strategy": "cheapest",
      "settlements": [
        {
          "chainName": "EVM",
          "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
          "chainId": "1",
          "symbol": "USDC",
          "tokenDecimals": 6
        }
      ]
    },
    "destinationConfig": {
      "destinations": [
        {
          "chainName": "EVM",
          "type": "address",
          "identifier": "0xYourSettlementAddress"
        }
      ]
    },
    "memo": {
      "description": "Payment for order #1234"
    }
  }'
```

The response includes `flow.id`: pass this as the `flowId` to your Flutter app when calling the SDK methods below.

> [!NOTE]
>
> **Optional: add your own fee.** Add a `feeConfig` to the create body to take a percentage of the payment for yourself or for your own customers. Recipients can be EVM or Solana addresses, with at most one recipient per address family. The fee applies to deposit-address payments and to wallet payments that need a swap. Direct wallet transfers and exchange payments carry no fee. [Adding your own fee](https://docs.dynamic.xyz/docs/flow/adding-your-own-fee.md) explains when the fee applies, and the [Fees API](https://docs.dynamic.xyz/docs/flow/fees.md) gives the config shape and how to claim fees.

> [!NOTE]
>
> **Optional: generate a payment link instead.** If you'd rather send the payer a URL than wire the create step into your own app, call [`POST /server/{environmentId}/payment-links`](https://docs.dynamic.xyz/docs/flow/api#payment-links) instead of the create-flow endpoint above. It takes the same body plus a required `baseUrl`, and returns a `paymentUrl` you can send directly. Decode the `flow` query parameter on that page to recover the `flowId`, then continue with the SDK methods below starting at source attachment. See [Payment Links](https://docs.dynamic.xyz/docs/flow/payment-links.md) for the hosted, self-hosted, and custom domain options for `baseUrl`.

> [!NOTE]
>
> **Optional: disable swaps.** Add `disableSwaps: true` to the create body when the source must already match a configured settlement. See [Disable swaps](https://docs.dynamic.xyz/docs/flow/api.md#disable-swaps) for quote-time behavior and error handling, and [Accept a same-token payment](https://docs.dynamic.xyz/docs/flow/same-token-payment.md) for a complete example.

## Installation

The `dynamic_sdk_fireblocks_flow` package is available from Dynamic Flutter SDK `1.14.0` onward. Add it to your `pubspec.yaml`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk: ^1.15.1
  dynamic_sdk_fireblocks_flow: ^1.15.1
```

For EVM sources you also need `dynamic_sdk_web3dart`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk_web3dart: ^1.15.1
```

For Solana sources you need `dynamic_sdk_solana`:

```yaml theme={"system"}
dependencies:
  dynamic_sdk_solana: ^1.15.1
```

Then run:

```bash theme={"system"}
flutter pub get
```

## Supported chains

The Flutter SDK supports the following source chains:

* BTC
* EVM
* SOL
* SUI
* TRON

> [!NOTE]
>
> TRON cross-chain swaps currently route through intent-based bridges and may incur higher gas costs relative to the transfer amount.

## Overview

The client-side flow follows these steps:

1. **Attach** the source wallet or deposit address with `attachSource`.
2. **Quote** the conversion with `getQuote`.
3. **Prepare** the unsigned source-chain payload with `prepareSigning`.
4. **Sign and broadcast** the payload with the source wallet's own signer.
5. **Record** the resulting transaction hash with `broadcast`.
6. **Poll** for completion with `getFlow`.

`attachSource` returns a session token (`dft_…`). The SDK persists it to secure storage keyed by `flowId` and adds it automatically to every subsequent call. You never need to handle it directly.

## Resume a flow from its current state

A `flowId` represents one execution attempt. On page load, retry, or reconnect, call `getFlow` before calling a mutation. Do not unconditionally call `attachSource` again.

| `executionState` | Valid next action |
| :- | :- |
| `initiated` | Attach a source. Only your server can cancel a flow in this state. |
| `source_attached` | Attach a different source, get a quote, or cancel. For an exchange source, such as Coinbase, the payer completes the purchase through the exchange's purchase link, and you poll the flow. |
| `quoted` | Re-quote, attach a different source, prepare signing for a wallet, or cancel. For a deposit address, poll until the deposit is seen. |
| `signing` | Continue the in-progress signing operation. If it cannot resume, re-quote, attach another source, or cancel. |
| `broadcasted` or `source_detected` | Poll or process webhooks. The payment is on its way, so no one can cancel the flow or change its source. |
| `source_confirmed` | Poll or process webhooks until settlement completes or fails. Do not call execution mutations. |
| `cancelled`, `expired`, or `failed` | Create a new flow for another attempt. These execution states do not allow any further transitions. |

> [!WARNING]
>
> `source_confirmed` means source execution is finished, not necessarily that settlement is finished. Keep tracking `settlementState` until it reaches `completed` or `failed`.

[Expiry and cancellation](https://docs.dynamic.xyz/docs/flow/expiry-and-cancellation.md) explains when a flow expires and who can cancel it.

## Full example

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_fireblocks_flow/dynamic_sdk_fireblocks_flow.dart';
import 'package:flutter/material.dart';

// Initialized earlier in main()
// DynamicSDK.init(
//   props: ClientProps(
//     environmentId: 'YOUR_ENVIRONMENT_ID',
//     appName: 'Your App',
//     appOrigin: 'https://your-app.com',
//   ),
// );

Future<void> runFlow({
  required String flowId,
  required BaseWallet wallet,
}) async {
  final flow = DynamicSDK.instance.fireblocksFlow;

  // Step 1: attach the source wallet
  final attached = await flow.attachSource(
    flowId: flowId,
    request: AttachSourceRequest(
      sourceType: FlowSourceType.wallet,
      fromAddress: wallet.address,
      fromChainId: '1',
      fromChainName: ChainEnum.evm,
    ),
  );

  // Step 2: get a quote. Quotes expire 60 seconds after creation.
  final quoted = await flow.getQuote(
    flowId: flowId,
    request: const GetQuoteRequest(
      fromTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
    ),
  );

  print('Quote: ${quoted.quote}');

  // Step 3: prepare the unsigned source-chain payload
  final prepared = await flow.prepareSigning(flowId: flowId);

  // Step 4: sign and broadcast with the source wallet's signer
  // (see chain-specific examples below)
  final txHash = await signAndBroadcast(prepared.signingPayload, wallet);

  // Step 5: record the broadcast
  await flow.broadcast(
    flowId: flowId,
    request: BroadcastRequest(txHash: txHash),
  );

  // Step 6: poll until settlement finishes or the execution ends
  var status = await flow.getFlow(flowId: flowId);
  while (
    status.settlementState != FlowSettlementState.completed &&
    status.settlementState != FlowSettlementState.failed &&
    status.executionState != FlowExecutionState.cancelled &&
    status.executionState != FlowExecutionState.expired &&
    status.executionState != FlowExecutionState.failed
  ) {
    await Future.delayed(const Duration(seconds: 3));
    status = await flow.getFlow(flowId: flowId);
  }

  print('Final settlement state: ${status.settlementState}');
}
```

`flowId` and `wallet` come from your app state. `wallet` is a `BaseWallet` returned by the Dynamic SDK.

## Chain-specific signing

Step 4 (sign and broadcast) is not an API call. You sign `prepared.signingPayload` with the source wallet's own signer and submit the result to the chain. The `SigningPayload` exposes one chain-specific field:

* `EVM` → `evmTransaction` (and optional `evmApproval`)
* `SOL` / `SUI` → `serializedTransaction` (base64)
* `BTC` → `psbt` (base64-encoded unsigned PSBT)
* `TRON` → `tronTransaction`

### EVM source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_web3dart/dynamic_sdk_web3dart.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final evmTx = payload.evmTransaction;
  if (evmTx == null) {
    throw Exception('No EVM transaction in signing payload');
  }

  final transaction = Transaction(
    from: EthereumAddress.fromHex(wallet.address),
    to: EthereumAddress.fromHex(evmTx.to),
    data: evmTx.data.isEmpty ? null : hexToBytes(evmTx.data),
    value: EtherAmount.inWei(BigInt.parse(evmTx.value)),
    maxGas: evmTx.gasLimit != null ? int.tryParse(evmTx.gasLimit!) : null,
  );

  final txHash = await DynamicSDK.instance.web3dart.sendTransaction(
    transaction: transaction,
    wallet: wallet,
  );

  return txHash;
}
```

If `evmApproval` is present, submit the ERC-20 approval transaction first and wait for it to be mined before signing the main `evmTransaction`.

### Solana source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:dynamic_sdk_solana/dynamic_sdk_solana.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final serialized = payload.serializedTransaction;
  if (serialized == null) {
    throw Exception('No serialized transaction in signing payload');
  }

  final signer = DynamicSDK.instance.solana.createSigner(wallet: wallet);
  final txHash = await signer.signAndSendEncodedTransaction(
    base64Transaction: serialized,
  );

  return txHash;
}
```

### Bitcoin source

```dart theme={"system"}
import 'dart:convert';

import 'package:dynamic_sdk/dynamic_sdk.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final psbtBase64 = payload.psbt;
  if (psbtBase64 == null) {
    throw Exception('No PSBT in signing payload');
  }

  final psbtBytes = base64.decode(psbtBase64);

  // Sign the PSBT with your Bitcoin signer library, then finalize and
  // broadcast the resulting transaction to the Bitcoin network.
  // This step is not provided by the Dynamic SDK.
  final txHash = await signPsbtAndBroadcast(psbtBytes, wallet);

  return txHash;
}
```

### TRON source

```dart theme={"system"}
import 'package:dynamic_sdk/dynamic_sdk.dart';

Future<String> signAndBroadcast(
  SigningPayload payload,
  BaseWallet wallet,
) async {
  final tronTx = payload.tronTransaction;
  if (tronTx == null) {
    throw Exception('No TRON transaction in signing payload');
  }

  // Sign the raw transaction with your TRON signer and broadcast it to the
  // TRON network. This step is not provided by the Dynamic SDK.
  final txHash = await signTronTransactionAndBroadcast(tronTx, wallet);

  return txHash;
}
```

## Deposit address source

With a deposit address flow the user sends funds directly to a generated address: no wallet connection or on-chain signing required from your app. Works for BTC, SOL, EVM, and TRON.

**1. Attach a deposit address source**

```dart theme={"system"}
await flow.attachSource(
  flowId: flowId,
  request: const AttachSourceRequest(
    sourceType: FlowSourceType.depositAddress,
    fromChainId: '1',
    fromChainName: ChainEnum.evm,
    refundAddress: '0xRefundAddress',
  ),
);
```

**2. Get a quote: response includes the deposit address**

```dart theme={"system"}
final quoted = await flow.getQuote(
  flowId: flowId,
  request: const GetQuoteRequest(
    fromTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  ),
);

final depositAddress = quoted.quote.depositAddress;
final rawAmount = quoted.quote.fromAmount;

// Show the address and amount to the user as a QR code or copy string.
```

The quoted amount is valid for 60 seconds. The address stays open until the flow ends, but the deposit provider guarantees the quoted amount for only 60 seconds. If the user has not sent funds by then, call `getQuote` again to refresh the amount. A new quote for the same chain and token usually keeps the same address.

**3. Poll until the transfer is detected**

Dynamic watches the address. Poll `getFlow` until `executionState` leaves `quoted`.

```dart theme={"system"}
var status = await flow.getFlow(flowId: flowId);
while (status.executionState == FlowExecutionState.quoted) {
  await Future.delayed(const Duration(seconds: 3));
  status = await flow.getFlow(flowId: flowId);
}

print('Execution state: ${status.executionState}');
```

The flow moves to `source_detected` when the deposit is seen, and then to `source_confirmed` when it is confirmed. If no deposit arrives before the flow expires, it moves to `expired`. After it leaves `quoted`, keep polling as shown in [Polling for status](#polling-for-status) until settlement completes or fails.

> [!NOTE]
>
> There is no signing or broadcast step for a deposit address, and calling `broadcast` on a deposit-address flow fails. Funds sent to the address after the flow is cancelled or expired can still reach your destination without the flow recording them. See [Expiry and cancellation](https://docs.dynamic.xyz/docs/flow/expiry-and-cancellation.md#funds-sent-after-cancellation-or-expiry).

## Resuming after an app restart

The session token returned by `attachSource` is persisted to secure storage automatically. To resume tracking an in-flight flow after the app was killed, call `getFlow`: it needs no session token:

```dart theme={"system"}
final status = await DynamicSDK.instance.fireblocksFlow.getFlow(
  flowId: flowId,
);
```

If a later call throws `FireblocksFlowException` with `sessionTokenInvalid == true`, the stored token has expired or was rejected and has already been removed. Before the payment is sent, call `attachSource` again to obtain a new token. After the payment is sent, the app only needs to read the flow with `getFlow`.

## Error handling

All Fireblocks Flow methods throw `FireblocksFlowException` on failure. The exception exposes `message`, `statusCode`, and `sessionTokenInvalid`. Use `sessionTokenInvalid` to decide whether to re-run `attachSource`.

```dart theme={"system"}
try {
  final quoted = await flow.getQuote(
    flowId: flowId,
    request: const GetQuoteRequest(fromTokenAddress: '0x...'),
  );
} on FireblocksFlowException catch (e) {
  if (e.sessionTokenInvalid) {
    // Token removed from storage; re-attach
    await flow.attachSource(
      flowId: flowId,
      request: AttachSourceRequest(
        sourceType: FlowSourceType.wallet,
        fromAddress: wallet.address,
        fromChainId: '1',
        fromChainName: ChainEnum.evm,
      ),
    );
  } else {
    print('Flow error: ${e.message}');
  }
}
```

### Common errors

| Status | Error | Cause | Fix |
| :- | :- | :- | :- |
| `400` | `fromAddress, fromChainId, and fromChainName are required for wallet sources` | Missing source fields when `sourceType` is `wallet`. | Pass `fromAddress`, `fromChainId`, and `fromChainName` from the wallet. |
| `400` | `fromChainId, fromChainName, and refundAddress are required for deposit_address sources` | Missing chain or refund fields for deposit-address sources. | Pass `fromChainId`, `fromChainName`, and `refundAddress`. |
| `400` | `fromAddress is not accepted for deposit_address sources; use refundAddress instead` | `fromAddress` was passed with `depositAddress` source. | Remove `fromAddress` and use `refundAddress`. |
| `403` | `Flow is blocked by sanctions` | The wallet was blocked by [screening](https://docs.dynamic.xyz/docs/flow/screening.md). Only wallet sources are screened. | Attach a different source. |
| `404` | `Flow not found` | The `flowId` does not exist or the environment ID is wrong. | Verify the `flowId` and environment. |
| `409` | State transition error | The endpoint was called from an invalid flow state. | Call `getFlow` and resume from the current `executionState`. |
| `422` | `Quote has expired; request a new quote before signing` | The quote is older than 60 seconds. | Call `getQuote` again, then retry `prepareSigning`. |
| `422` | `Insufficient balance ...` | The wallet does not have enough tokens or gas. | Fund the wallet or use a smaller amount. |

[API errors](https://docs.dynamic.xyz/docs/flow/api-errors.md) lists every rejection the API returns for each request.

## Polling for status

After `broadcast`, poll `getFlow` to track execution and settlement. Stop when `settlementState` reaches `completed` or `failed`, or when `executionState` reaches a terminal state (`cancelled`, `expired`, `failed`).

```dart theme={"system"}
Future<FlowStatus> pollUntilDone(FireblocksFlow flow, String flowId) async {
  const terminalExecution = [
    FlowExecutionState.cancelled,
    FlowExecutionState.expired,
    FlowExecutionState.failed,
  ];
  const terminalSettlement = [
    FlowSettlementState.completed,
    FlowSettlementState.failed,
  ];

  Future<FlowStatus> poll() async {
    final status = await flow.getFlow(flowId: flowId);

    if (terminalExecution.contains(status.executionState) ||
        terminalSettlement.contains(status.settlementState)) {
      return status;
    }

    await Future.delayed(const Duration(seconds: 3));
    return poll();
  }

  return poll();
}
```

## Related

* [Flow payments](https://docs.dynamic.xyz/docs/flow/payments.md): Product overview and concepts
* [Payment lifecycle and states](https://docs.dynamic.xyz/docs/flow/lifecycle-walkthrough.md): Execution, settlement, and risk states
* [Expiry and cancellation](https://docs.dynamic.xyz/docs/flow/expiry-and-cancellation.md): When a flow expires and who can cancel it
* [Failures and refunds](https://docs.dynamic.xyz/docs/flow/failures-and-refunds.md): Failure codes and provider refunds
* [Screening](https://docs.dynamic.xyz/docs/flow/screening.md): What is screened and what a block does
* [Flow webhooks](https://docs.dynamic.xyz/docs/flow/webhooks.md): Server-side state change events
* [Flow payments API guide](https://docs.dynamic.xyz/docs/flow/api.md): Full HTTP API guide
* [Flutter SDK reference](https://docs.dynamic.xyz/docs/flutter/sdk-reference/overview.md): Flutter SDK reference
* [Web3Dart package](https://docs.dynamic.xyz/docs/flutter/web3dart.md): EVM transactions with `dynamic_sdk_web3dart`
* [Solana interactions](https://docs.dynamic.xyz/docs/flutter/solana.md): Solana transactions with `dynamic_sdk_solana`


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