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

# Fund from an exchange

> Open the hosted Connections page with intent=fundFromExchange to withdraw from a user's Coinbase account to an address, or to read their exchange balances.

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

[Connections](https://www.dynamic.xyz/docs/connections/overview.md) can also move funds out of a user's exchange account. Open the hosted page with `?intent=fundFromExchange` and it signs the user in to the exchange over OAuth, reads their balances, and (in the default mode) asks the exchange to withdraw to an address you name. Coinbase is the only exchange wired today.

This is not a wallet operation. No wallet is connected, nothing is signed on device, and the exchange itself broadcasts the withdrawal.

The intent has two modes:

> [card] transfer (default)
>
> Link, pick a balance, confirm, and the exchange sends the funds. Returns the exchange's transfer id.

> [card] balances
>
> Link and read only. Returns the balance list after the user agrees to share it. Nothing moves.

> [!NOTE]
>
> There is no headless path for either mode. OAuth has nowhere to run inside the hidden web view used by headless Connections: a popup has no surface, and a cross-origin redirect is refused by its navigation delegate. Both modes run in the system browser (`ASWebAuthenticationSession` on iOS, Chrome Custom Tabs on Android).

## What you need

* The exchange enabled for your Dynamic environment in the dashboard funding settings. Without it, the flow returns an error at the link step.
* A `redirect_uri` you control: your app's custom scheme for native callers, or an allow-listed `http(s)` host (see [Securing the redirect](https://www.dynamic.xyz/docs/connections/overview.md#securing-the-redirect)).
* For `mode=balances` with an `http(s)` redirect, a configured redirect host allow-list. The page refuses the request otherwise.

The flow is never silent. Expect the browser to open, an exchange sign-in, and one or two further prompts on the page: two-step verification if the account has it on, and recipient details when the jurisdictions involved require them.

## Request parameters

Add these to your hosted Connections page URL, alongside the `redirect_uri`, `nonce`, and `environmentId` parameters that every Connections request takes.

| Parameter | Required | Description |
| - | - | - |
| `intent` | Yes | `fundFromExchange`. |
| `exchange` | No | Catalogue key. Defaults to `coinbase`, the only supported value today. |
| `mode` | No | `transfer` (default) or `balances`. |
| `to` | Yes for transfer | Destination address. Not validated by the page: an exchange withdrawal can target any chain the exchange supports (an EVM `0x…`, a Solana base58 key, a BTC address). The exchange refuses the transfer if the address does not match `currency` and `network`. Ignored in `balances` mode. |
| `currency` | No | Token symbol to debit, for example `USDC`. When it matches exactly one balance, the page skips its balance picker. |
| `amount` | No | Decimal string prefill, for example `12.5`. The user still confirms it, and the page refuses more than the available balance. |
| `network` | No | Network slug passed through to the exchange, for example `base`. The exchange, not the page, decides which chain the funds land on. |

```ts theme={"system"}
"https://connect.dynamicauth.com/" +
 "?intent=fundFromExchange" +
 "&exchange=coinbase" +
 "&to=0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b" +
 "&currency=USDC" +
 "&amount=12.5" +
 "&network=base" +
 "&redirect_uri=myapp://wallet-callback" +
 "&nonce=a1b2c3d4e5"
```

## Result parameters

The page redirects back to your `redirect_uri` with the result as query parameters, and always echoes `nonce` so a caller with several requests in flight can tell them apart. Compare it against the value you sent and drop the result if it differs.

A `transfer` success:

| Parameter | Description |
| - | - |
| `transferId` | The exchange's transfer id. This is not an on-chain transaction hash. The exchange broadcasts on its own schedule, so there may be no hash yet at all. Poll the exchange or the destination address for settlement. |
| `exchange` | Which exchange was debited, for example `coinbase`. |
| `amount` | Amount the exchange accepted. Can differ from what you requested, for example rounded to the currency's precision. |
| `currency` | Currency the exchange accepted. |
| `status` | The exchange's own status string when it reports one, for example `pending`. |

A `balances` success:

| Parameter | Description |
| - | - |
| `exchange` | Which exchange the balances came from. |
| `accounts` | JSON array of balances. Each entry has `accountName`, `currency`, `balance`, and `available`. Show `available`, which is what the user can actually move. |

A failure in either mode:

| Parameter | Description |
| - | - |
| `error` | `1`. |
| `code` | Stable machine code. See the codes below. |
| `message` | Human-readable reason, safe to surface to the user. |

| Code | Meaning |
| - | - |
| `transfer_failed` | The exchange refused the withdrawal, or it failed while the page was driving it. `message` carries the reason. |
| `mfa_attempts_exhausted` | Three verification codes were rejected. The page stops there so the exchange does not rate-limit the account. Start a new request. |
| `balances_failed` | The balance read failed, most often because the exchange is not linked or not enabled for this environment. |

If the user dismisses the browser instead of finishing, no redirect fires at all. Treat that as a cancel, not an error.

## Read balances instead

`mode=balances` runs the same OAuth link step, then stops. It never asks the exchange for a transfer, and it needs no `to`, `amount`, or `network`. Use it to show the user what they hold before you ask them to fund anything.

The page returns only after the user taps through a screen naming who receives the list. A failed read returns on its own, since it carries nothing sensitive.

> [!NOTE]
>
> Flutter is the only native wrapper for `mode=balances` today. In Swift and Kotlin, open the hosted page with `mode=balances` through the same session API the transfer flow uses and parse the `accounts` parameter off the callback.

## Add it to your app

The fund methods ship inside the same copy-paste sources the [platform guides](https://www.dynamic.xyz/docs/connections/overview.md) publish for basic connect, so there is nothing new to add and nothing new in your dependency list. If you already followed your platform's Connections guide, refresh your copy of its harness file and reuse the callback scheme you registered there.

> [tab] Flutter
>
> **1. Use the harness file.** The `fireblocks_connect.dart` on [Connections on Flutter](https://www.dynamic.xyz/docs/connections/flutter.md) already includes `FireblocksConnect.fundFromExchange` and `FireblocksConnect.getExchangeBalances`, and its `models.dart` carries the `ExchangeTransferResult` and `ExchangeBalancesResult` types. Re-copy the files from that guide, or pull just those additions into your copy.
>
> **2. Reuse your callback scheme.** Both methods redirect to `<scheme>://wallet-callback`, the same target basic connect uses, so the `Info.plist` entry and the `AndroidManifest.xml` intent filter from the Flutter guide already cover this flow. `flutter_web_auth_2` is the only dependency involved.
>
> **3. Fund an address.**
>
> ```dart fund_button.dart theme={"system"}
> import 'package:flutter_app/src/fireblocks_connect.dart';
>
> try {
>   final result = await FireblocksConnect.fundFromExchange(
>     hostedPageUrl: 'https://connect.dynamicauth.com/',
>     scheme: 'myapp',
>     to: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
>     currency: 'USDC',
>     amount: '12.5',
>     network: 'base',
>     environmentId: 'b1e3aca9-0646-411a-b4ab-c31ce49935b3',
>   );
>
>   switch (result) {
>     case ExchangeTransferSuccess(:final transferId, :final amount, :final currency):
>       // transferId is the exchange's id, not an on-chain hash
>       break;
>     case ExchangeTransferFailure(:final error):
>       // error.code, error.message
>       break;
>   }
> } on FireblocksConnectCancelled {
>   // user dismissed the browser
> } on FireblocksConnectError catch (e) {
>   // e.message: nonce mismatch or malformed callback
> }
> ```
>
> **4. Or read balances only.**
>
> ```dart balances_button.dart theme={"system"}
> final result = await FireblocksConnect.getExchangeBalances(
>   hostedPageUrl: 'https://connect.dynamicauth.com/',
>   scheme: 'myapp',
>   environmentId: 'b1e3aca9-0646-411a-b4ab-c31ce49935b3',
> );
>
> switch (result) {
>   case ExchangeBalancesSuccess(:final accounts):
>     for (final account in accounts) {
>       print('${account.available} ${account.currency} in ${account.accountName}');
>     }
>   case ExchangeBalancesFailure(:final error):
>     // error.code, error.message
> }
> ```

> [tab] Swift
>
> **1. Use the harness file.** The `FireblocksConnectFlow.swift` on [Connections on iOS](https://www.dynamic.xyz/docs/connections/ios.md) already includes `FireblocksFundFromExchangeFlow`, a self-contained `ASWebAuthenticationSession` flow with its own result and error types. Re-copy the file from that guide, or pull just that type into your copy. It needs nothing from the connect flow except your scheme.
>
> **2. Reuse your callback scheme, and forward the return URL.** The `CFBundleURLTypes` entry from the iOS guide already covers `<scheme>://wallet-callback`. Add the fund flow to the same `onOpenURL` handler, because the exchange can finish the sign-in outside the session and hand the result back through the scheme.
>
> ```swift App.swift theme={"system"}
> WindowGroup {
>     ContentView()
>         .onOpenURL { url in
>             if FireblocksFundFromExchangeFlow.handleCallbackURL(url) { return }
>             FireblocksConnectFlow.handleCallbackURL(url)
>         }
> }
> ```
>
> **3. Fund an address.**
>
> ```swift FundButton.swift theme={"system"}
> FireblocksFundFromExchangeFlow.fund(
>     hostedPageURL: URL(string: "https://connect.dynamicauth.com/")!,
>     scheme: "myapp",
>     to: "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
>     currency: "USDC",
>     amount: "12.5",
>     network: "base",
>     environmentId: "b1e3aca9-0646-411a-b4ab-c31ce49935b3"
> ) { result in
>     switch result {
>     case .success(let transfer):
>         // transfer.transferId, transfer.amount,
>         // transfer.currency, transfer.status
>         break
>     case .failure(.cancelled):
>         break
>     case .failure(let error):
>         // .nonceMismatch, .malformedResult,
>         // or .failed(code:message:) reported by the page
>         break
>     }
> }
> ```
>
> `fund` returns the flow object it starts. Hold onto it only if you want to cancel the session yourself; it keeps itself alive until the callback fires.

> [tab] Kotlin
>
> **1. Use the harness file.** The `FireblocksConnect.kt` on [Connections on Android](https://www.dynamic.xyz/docs/connections/android.md) already includes `FireblocksConnect.fundFromExchange`, the `FireblocksFundResult` types, and the `FireblocksRedirectActivity` dispatch branch that routes the return URL to `handleFundRedirect`. Re-copy the file from that guide, or pull just those additions into your copy.
>
> **2. Reuse your return activity.** The redirect lands on `<scheme>://wallet-callback`, the same target the connect flow uses, so the `FireblocksRedirectActivity` entry from the Android guide needs no manifest change. `androidx.browser` is the only dependency involved.
>
> **3. Fund an address.**
>
> ```kotlin FundButton.kt theme={"system"}
> FireblocksConnect.fundFromExchange(
>     context = this,
>     hostedPageUrl = "https://connect.dynamicauth.com/",
>     scheme = "myapp",
>     to = "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b",
>     currency = "USDC",
>     amount = "12.5",
>     network = "base",
>     environmentId = "b1e3aca9-0646-411a-b4ab-c31ce49935b3",
> ) { result ->
>     when (result) {
>         is FireblocksFundResult.Success -> {
>             // result.transferId, result.amount,
>             // result.currency, result.status
>         }
>         is FireblocksFundResult.Cancelled -> Unit
>         is FireblocksFundResult.Error -> {
>             // result.code, result.reason
>         }
>     }
> }
> ```
>
> The callback arrives on the main thread from the return activity, so you can touch your UI directly in it.

> [tab] Web
>
> **1. Send the user to the hosted page.** There is nothing to copy: you build the URL and read the result off your own callback route.
>
> ```ts fund.ts theme={"system"}
> const nonce = crypto.randomUUID();
>
> const url = new URL("https://connect.dynamicauth.com/");
> url.searchParams.set("intent", "fundFromExchange");
> url.searchParams.set("to", "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b");
> url.searchParams.set("currency", "USDC");
> url.searchParams.set("network", "base");
> url.searchParams.set("redirect_uri", "https://your-app.com/wallet/callback");
> url.searchParams.set("nonce", nonce);
>
> sessionStorage.setItem("fundNonce", nonce);
> window.location.assign(url.toString());
> ```
>
> For a read-only request, set `mode` to `balances` and drop `to`, `currency`, `amount`, and `network`.
>
> **2. Verify the nonce and read the result** on your callback route.
>
> ```ts callback.ts theme={"system"}
> const params = new URLSearchParams(window.location.search);
>
> if (params.get("nonce") !== sessionStorage.getItem("fundNonce")) {
>   throw new Error("Nonce mismatch");
> }
>
> if (params.get("error") === "1") {
>   // params.get("code"), params.get("message")
> } else if (params.has("accounts")) {
>   const accounts = JSON.parse(params.get("accounts") ?? "[]") as Array<{
>     accountName: string;
>     currency: string;
>     balance: number;
>     available: number;
>   }>;
> } else {
>   // params.get("transferId"), params.get("amount"),
>   // params.get("currency"), params.get("status")
> }
> ```

## Security

> [!WARNING]
>
> `mode=balances` with an `http(s)` `redirect_uri` requires a configured redirect host allow-list. The page refuses the request when the allow-list is unset, because an attacker-chosen host would otherwise receive a linked user's whole balance list. Custom app schemes are never host-filtered and are unaffected. See [Securing the redirect](https://www.dynamic.xyz/docs/connections/overview.md#securing-the-redirect).

* **The user always confirms.** A transfer takes an explicit confirm step, and a balance read takes an explicit share step that names the receiving host or scheme. Neither mode returns data without a tap.
* **Restrict `environmentId` in production.** Every value the OAuth request is built from comes from that environment's own project settings, so an unrestricted `?environmentId=` lets a caller run the exchange consent screen under an environment you do not own. Allow-list the environment IDs your deployment accepts.
* **Balances carry no account identifier.** The result reports account names and amounts only, so the callback URL holds nothing that addresses a specific exchange account. Custom-scheme callbacks can land in operating system navigation logs.
* **Verification attempts are capped.** Three rejected codes end the flow with `mfa_attempts_exhausted` instead of letting the exchange lock the account out.

## Common pitfalls

* **`transferId` is not a transaction hash.** Do not paste it into a block explorer. Poll the exchange or the destination address for settlement.
* **The exchange owns the network choice.** A wrong `network` slug can land funds on a chain your user did not expect, or be refused outright. Send the slug the exchange uses.
* **The exchange must be enabled for the environment.** If the link step fails immediately, check the funding settings for that environment in the dashboard before debugging the client.
* **`amount` is a prefill, not a commitment.** The exchange can round it, and the returned `amount` is the one that was accepted.


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