> ## Documentation Index
> Fetch the complete documentation index at: https://public-openfx.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Verified accounts

> Pre-configure your bank accounts and stablecoin wallets so withdrawals only need an `withdrawalAccountId` and a `withdrawalAmount`.

A verified withdrawal account is a pre-approved bank account or stablecoin wallet that outgoing funds can settle to: fiat on a specific rail, or stablecoin on a specific chain. Every [`POST /v3/fx/withdrawals`](/v3/api-reference/trade-settlement/create-withdrawal) call references one via `withdrawalAccountId`. The currency and rail or chain are bound to the withdrawal account at creation time, so the withdrawal call itself stays minimal.

<Warning>
  **Verified accounts are managed in the dashboard.** Creation, edits, and
  removal happen in the [OpenFX dashboard](https://app.openfx.com). Your
  integration **reads** the resulting accounts via the API and references them
  by `id` on withdrawal requests.
</Warning>

## Why verify

* **Risk.** Withdrawal accounts are reviewed before they go live, so a compromised JWT can't send funds to a wallet you've never approved.
* **Simplicity.** Withdrawals don't need bank details, beneficiary names, IBANs, or chain identifiers on every call. That's all pinned to the account record.
* **Auditability.** Every dispatched withdrawal traces back to a specific `withdrawalAccountId`, which traces back to the human who approved it.

<Note>
  **Each verified account = one address × one network × one asset.** If you want to withdraw USDC **and** USDT to the same Ethereum address, you must add **two** records: one per `(address, network, asset)` tuple. Likewise, the same address on Ethereum mainnet vs Polygon is two separate records.

  **Why the constraint exists.** Customers funded via exchanges often don't control the same nominal address across all chains, and some exchanges issue different deposit addresses per token even on the same chain. Forcing an explicit per-tuple add prevents a withdrawal landing at an address that doesn't actually receive the asset on that chain: a class of permanent loss. If you self-custody and the same address works for every chain × asset, you still add each tuple explicitly.
</Note>

## Add an account (dashboard)

<Steps>
  <Step title="Sign in to the dashboard">
    Go to [your accounts](https://app.openfx.com/your-accounts/accounts) if you have admin access. Withdrawal account management is gated to admins.
  </Step>

  <Step title="Pick an account type">
    **Fiat bank account** (API `assetType: "FIAT"`) for a wire, SEPA, SPEI, or similar rail, or **stablecoin wallet** (API `assetType: "CRYPTO"`) for an on-chain account.
  </Step>

  <Step title="Enter the account details">
    **For fiat**: currency, beneficiary name, account number / IBAN, routing number / BIC, and the rail you want OpenFX to dispatch over. The selectable rails depend on the currency (e.g. USD shows Fedwire and SWIFT; EUR shows SEPA, SEPA Instant, and SWIFT).

    **For stablecoin**: token (USDC, USDT, EURC), the chain (one of the [supported networks](/v3/supported-networks)), and the wallet address. The address must match the chain's format. Solana addresses are not interchangeable with Ethereum addresses.
  </Step>

  <Step title="Attach supporting documents">
    Upload up to **3 files, 10 MB each**. For fiat accounts, attach bank statements or beneficiary-ownership documents. For stablecoin wallets, attach proof of address control (signed message, exchange screenshot, or similar). Documents stay in the dashboard. The API never returns them.
  </Step>

  <Step title="Submit for review">
    Every submission passes a two-step gate before going live:

    * **Compliance**: AML screening, plus on-chain TRM screening for stablecoin wallets.
    * **Ops**: bank-detail validation and beneficiary-ownership review.

    Both gates must clear before the account becomes active. The listing on [`GET /v3/fx/withdrawal-accounts`](/v3/api-reference/trade-settlement/list-withdrawal-accounts) includes accounts in every status (<span class="ofx-enum">PENDING</span>, <span class="ofx-enum">ACTIVE</span>, <span class="ofx-enum">ARCHIVED</span>, <span class="ofx-enum">REJECTED</span>, <span class="ofx-enum">DEACTIVATED</span>, <span class="ofx-enum">ADDITIONAL\_ACTION\_NEEDED</span>) by default; pass `?status=ACTIVE` (or filter client-side) based on your flow. Only <span class="ofx-enum">ACTIVE</span> accounts accept withdrawals. Submitting against any other status returns an error.

    <Note>
      `GET /v3/fx/withdrawal-accounts` supports `?status=` (plus `?assetType=`, `?currency=`, `?network=`, `?rail=`, and `?verified=`) as server-side filters. Filter to <span class="ofx-enum">ACTIVE</span> (via the query parameter or client-side) before posting withdrawals.
    </Note>
  </Step>
</Steps>

<Note>
  **Duplicates are rejected at submission.** Two records with the same rail,
  currency, and account number (or wallet address) cannot coexist. If a
  withdrawal account is already on file, edit the existing record instead of
  resubmitting.
</Note>

## Read accounts via the API

Once approved, accounts appear on [`GET /v3/fx/withdrawal-accounts`](/v3/api-reference/trade-settlement/list-withdrawal-accounts).

<CodeGroup>
  ```javascript Node theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from /v3/verified-accounts#read-accounts-via-the-api.
  // signRequest is the real helper from /v3/authentication — adjust this path
  // if you've copied it somewhere else in your own project.
  const { signRequest } = require("../../authentication/node/sign-request");
  // Picks the crypto withdrawal account for a currency + chain. Kept separate
  // from the network call so it's testable without a real request.
  function findAccount(accounts, currency, network) {
    return accounts.find((a) => a.currency === currency && a.network === network);
  }
  async function listAccounts() {
    const url = "https://api.openfx.com/v3/fx/withdrawal-accounts";
    const { headers } = signRequest({ method: "GET", url });
    const res = await fetch(url, { headers });
    const { data } = await res.json();
    return data;
  }
  module.exports = { findAccount, listAccounts };
  if (require.main === module) {
    listAccounts()
      .then((accounts) => {
        const usdcOnSolana = findAccount(accounts, "USDC", "SOLANA");
        console.log(usdcOnSolana);
      })
      .catch((err) => {
        console.error("Error:", err.message);
        process.exitCode = 1;
      });
  }
  ```

  ```python Python theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  """Reads verified withdrawal accounts and picks the right one for a
  withdrawal — the pattern from
  /v3/verified-accounts#read-accounts-via-the-api."""
  import os
  import sys
  # sign_request is the real helper from /v3/authentication — adjust this path
  # if you've copied it somewhere else in your own project.
  sys.path.insert(
      0, os.path.join(os.path.dirname(__file__), "..", "..", "authentication", "python")
  )
  from sign_request import sign_request  # noqa: E402
  import requests  # noqa: E402
  def find_account(accounts, currency, network):
      """Picks the crypto withdrawal account for a currency + chain. Kept
      separate from the network call so it's testable without a real request."""
      return next((a for a in accounts if a["currency"] == currency and a["network"] == network), None)
  def list_accounts():
      url = "https://api.openfx.com/v3/fx/withdrawal-accounts"
      headers, _ = sign_request("GET", url)
      res = requests.get(url, headers=headers)
      return res.json()["data"]
  if __name__ == "__main__":
      accounts = list_accounts()
      print(find_account(accounts, "USDC", "SOLANA"))
  ```

  ```java Java theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/verified-accounts#read-accounts-via-the-api.
  import java.net.HttpURLConnection;
  import java.net.URI;
  import java.util.List;
  import java.util.Map;
  public class FindAccount {
      static class Account {
          final String id;
          final String currency;
          final String network;
          Account(String id, String currency, String network) {
              this.id = id;
              this.currency = currency;
              this.network = network;
          }
      }
      // Picks the crypto withdrawal account for a currency + chain. Kept
      // separate from the network call so it's testable without a real request.
      static Account findAccount(List<Account> accounts, String currency, String network) {
          for (Account a : accounts) {
              if (a.currency.equals(currency) && a.network.equals(network)) return a;
          }
          return null;
      }
      static List<Account> listAccounts() throws Exception {
          String url = "https://api.openfx.com/v3/fx/withdrawal-accounts";
          // SignRequest is the real helper from /v3/authentication, compiled
          // on the classpath alongside this file — see that page's example.
          Map<String, String> headers = SignRequest.signRequest("GET", url, new byte[0]);
          HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
          conn.setRequestMethod("GET");
          for (Map.Entry<String, String> h : headers.entrySet()) {
              conn.setRequestProperty(h.getKey(), h.getValue());
          }
          conn.getResponseCode();
          // A real implementation would parse the JSON response body's `data`
          // array here.
          return List.of();
      }
      public static void main(String[] args) throws Exception {
          List<Account> accounts = listAccounts();
          System.out.println(findAccount(accounts, "USDC", "SOLANA"));
      }
  }
  ```

  ```cpp C++ theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/verified-accounts#read-accounts-via-the-api. Requires
  // OpenSSL and libcurl.
  #include <curl/curl.h>
  #include <optional>
  #include <string>
  #include <vector>
  // Matches the definition in ../../authentication/cpp/sign_request.cpp,
  // compiled together with this file — see that file for the real
  // implementation of signRequest.
  struct SignedRequest {
      std::string authHeader;
      std::string sigHeader;
  };
  SignedRequest signRequest(const std::string& method, const std::string& urlPath,
                             const std::string& query, const std::string& body);
  struct Account {
      std::string id;
      std::string currency;
      std::string network;
  };
  // Picks the crypto withdrawal account for a currency + chain. Kept separate
  // from the network call so it's testable without a real request.
  std::optional<Account> findAccount(const std::vector<Account>& accounts, const std::string& currency,
                                      const std::string& network) {
      for (const auto& a : accounts) {
          if (a.currency == currency && a.network == network) return a;
      }
      return std::nullopt;
  }
  static size_t discard(char*, size_t s, size_t n, void*) { return s * n; }
  std::vector<Account> listAccounts() {
      std::string url = "https://api.openfx.com/v3/fx/withdrawal-accounts";
      SignedRequest req = signRequest("GET", "/v3/fx/withdrawal-accounts", "", "");
      CURL* curl = curl_easy_init();
      curl_slist* headers = nullptr;
      headers = curl_slist_append(headers, ("Authorization: " + req.authHeader).c_str());
      headers = curl_slist_append(headers, ("X-Request-Signature: " + req.sigHeader).c_str());
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard);
      curl_easy_perform(curl);
      curl_slist_free_all(headers);
      curl_easy_cleanup(curl);
      // A real implementation would parse the JSON response body's `data`
      // array here.
      return {};
  }
  #ifndef FIND_ACCOUNT_NO_MAIN
  #include <iostream>
  int main() {
      try {
          auto accounts = listAccounts();
          auto account = findAccount(accounts, "USDC", "SOLANA");
          std::cout << (account ? account->id : "not found") << std::endl;
          return 0;
      } catch (const std::exception& e) {
          std::cerr << "Error: " << e.what() << std::endl;
          return 1;
      }
  }
  #endif
  ```

  ```go Go theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/verified-accounts#read-accounts-via-the-api.
  package main
  import (
  	"encoding/json"
  	"fmt"
  	"net/http"
  	// auth is the real signRequest helper from /v3/authentication — see
  	// go.mod's replace directive if you've copied it somewhere else.
  	auth "openfx-v3-authentication-example"
  )
  type Account struct {
  	ID       string `json:"id"`
  	Currency string `json:"currency"`
  	Network  string `json:"network"`
  }
  // Picks the crypto withdrawal account for a currency + chain. Kept separate
  // from the network call so it's testable without a real request.
  func findAccount(accounts []Account, currency, network string) *Account {
  	for _, a := range accounts {
  		if a.Currency == currency && a.Network == network {
  			return &a
  		}
  	}
  	return nil
  }
  func listAccounts() ([]Account, error) {
  	url := "https://api.openfx.com/v3/fx/withdrawal-accounts"
  	signed, err := auth.SignRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	for k, v := range signed.Headers {
  		req.Header.Set(k, v)
  	}
  	res, err := http.DefaultClient.Do(req)
  	if err != nil {
  		return nil, err
  	}
  	defer res.Body.Close()
  	var envelope struct {
  		Data []Account `json:"data"`
  	}
  	if err := json.NewDecoder(res.Body).Decode(&envelope); err != nil {
  		return nil, err
  	}
  	return envelope.Data, nil
  }
  func main() {
  	accounts, err := listAccounts()
  	if err != nil {
  		fmt.Println("Error:", err)
  		return
  	}
  	fmt.Println(findAccount(accounts, "USDC", "SOLANA"))
  }
  ```

  ```bash cURL theme={"theme":{"light":"poimandres","dark":"poimandres"}}
  #!/usr/bin/env bash
  # Reads verified withdrawal accounts and picks the right one for a
  # withdrawal — the pattern from
  # /v3/verified-accounts#read-accounts-via-the-api. Illustrative
  # only: cURL can't compute the ES256 request signature itself, so
  # $OPENFX_JWT/$OPENFX_SIGNATURE below are placeholders — get real values
  # from signRequest, in any supported language, via /v3/authentication.
  # X-App-Mode is hard-coded to SANDBOX below. To target Live, use a Live API
  # key (mint the JWT from it) and set X-App-Mode to LIVE.
  set -euo pipefail
  export OPENFX_JWT="<jwt-from-signRequest>"
  export OPENFX_SIGNATURE="<signature-from-signRequest>"
  ACCOUNTS=$(curl -s https://api.openfx.com/v3/fx/withdrawal-accounts \
    -H "Authorization: Bearer $OPENFX_JWT" \
    -H "X-Request-Signature: $OPENFX_SIGNATURE" \
    -H "X-App-Mode: SANDBOX")
  # Picks the crypto withdrawal account for a currency + chain. Filtering an
  # array by two fields needs real JSON parsing, unlike the single-field
  # extraction elsewhere in these examples. This is the one place these
  # scripts use jq (https://jqlang.org), not a hand-rolled pattern match.
  echo "$ACCOUNTS" | jq '.data[] | select(.currency == "USDC" and .network == "SOLANA")'
  ```
</CodeGroup>

A typical response, with a fiat row and a stablecoin row, looks like:

```json theme={"theme":{"light":"poimandres","dark":"poimandres"}}
{
  "data": [
    {
      "object": "withdrawalAccount",
      "id": "wac_NDqQ9LmcUASpnHR6CTvdkk",
      "assetType": "FIAT",
      "network": null,
      "rail": "FEDWIRE",
      "displayName": "Acme USD Reserves",
      "currency": "USD",
      "status": "ACTIVE",
      "verified": true,
      "creatorId": "usr_7m4VsfRw4pGrS76WYj5tnx",
      "destination": {
        "accountName": "Acme Reserves LLC",
        "accountNumber": "1234564321",
        "bankName": "JPMorgan Chase",
        "routingNumber": "021000021"
      },
      "createdAt": "2026-05-19T10:00:00.000Z",
      "updatedAt": "2026-05-19T10:00:00.000Z"
    },
    {
      "object": "withdrawalAccount",
      "id": "wac_7yE2Xr4vTqK9mB3sN6pW1",
      "assetType": "CRYPTO",
      "network": "SOLANA",
      "rail": null,
      "displayName": "Treasury USDC wallet",
      "currency": "USDC",
      "status": "ACTIVE",
      "verified": true,
      "creatorId": "usr_7m4VsfRw4pGrS76WYj5tnx",
      "destination": {
        "address": "5xZ8KqM3vN9pT4hR2bF7jL1wA6cD8eY5sU3xZ0q9Pn"
      },
      "createdAt": "2026-05-19T10:00:00.000Z",
      "updatedAt": "2026-05-19T10:00:00.000Z"
    }
  ],
  "pagination": {
    "limit": 100,
    "hasNext": false,
    "nextCursor": null,
    "hasPrev": false,
    "prevCursor": null
  }
}
```

Crypto accounts carry `network` (the chain, e.g. <span class="ofx-enum">SOLANA</span>) with a `null` `rail`; fiat accounts carry `rail` (one of <span class="ofx-enum">SWIFT</span>, <span class="ofx-enum">FEDWIRE</span>, <span class="ofx-enum">FPS</span>, or another local rail) with a `null` `network`. `network` determines the shape of `destination` for crypto (`destination.address`); `rail` does for fiat (`destination.accountName` / `accountNumber` / `bankName`, plus rail-specific fields like `swiftCode`, `routingNumber`, or `ukSortCode`). See the `WithdrawalAccount` schema in the [API reference](/v3/api-reference/trade-settlement/list-withdrawal-accounts) for the full per-rail shape.

<Tip>
  **Cache the account list, not the JWT.** Account membership changes on the
  order of days; tokens change every 60 seconds (and every request also carries
  a fresh `X-Request-Signature`). Pull `withdrawal_accounts` once at startup (or
  once per request when you need fresh state); don't ride a stale cache across
  new dashboard approvals.
</Tip>

## Use the `withdrawalAccountId` on a withdrawal

```bash theme={"theme":{"light":"poimandres","dark":"poimandres"}}
curl -X POST https://api.openfx.com/v3/fx/withdrawals \
  -H "Authorization: Bearer $OPENFX_JWT" \
  -H "X-Request-Signature: $OPENFX_SIGNATURE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "withdrawalAccountId": "a2c7d4f1-3b18-4f6a-9c35-2e8a1b4d9f02",
    "withdrawalAmount": "1000.00",
    "currency": "USDC"
  }'
```

The `currency` on the withdrawal must match the account's `currency`. The chain (for stablecoin) or rail (for fiat) is **not** on the request. Both are inferred from `withdrawalAccountId`.

## Lifecycle and changes

* **Locked fields.** Once an account is ACTIVE, the account number (or wallet address), rail or chain, and currency are locked. Changing any of them means creating a new account and re-running the two-step review.
* **Editable fields.** The **display name** (and a dashboard memo) can be edited without re-verification. The API returns the latest `displayName` on the next read.
* **Archiving.** Archive an account from the dashboard; the `id` stops accepting withdrawals immediately once `status` reads <span class="ofx-enum">ARCHIVED</span>. In-flight withdrawals against a now-archived account complete normally.
* **Unarchiving.** Unarchive the same `id` to bring it back; the UUID is stable, so no new account or `id` is created.

The dashboard is the **source of truth** for submission status, document history, and review-gate outcomes. The API returns the full account details nested under `destination` on the ACTIVE record; document attachments live in the dashboard.

## Common mistakes

* **Verifying a Polygon address and assuming USDC is fungible across chains.** A USDC withdrawal initiated against a Polygon `withdrawalAccountId` will only ever settle on Polygon. There is no cross-chain bridge inside OpenFX. See [Supported networks](/v3/supported-networks).
* **Picking the wrong rail for a fiat withdrawal account.** Fedwire vs SWIFT, or FPS vs SWIFT: these have different cut-offs, fee profiles, and value limits. The rail is fixed at account creation; pick it deliberately. CHAPS is a settlement route that OpenFX chooses, not a rail you select. See [Settlement times](/v3/settlement-times).
* **Polling the API for full review status.** The API surfaces a `status` (<span class="ofx-enum">PENDING</span>, <span class="ofx-enum">ACTIVE</span>, <span class="ofx-enum">ARCHIVED</span>, <span class="ofx-enum">REJECTED</span>, <span class="ofx-enum">DEACTIVATED</span>, <span class="ofx-enum">ADDITIONAL\_ACTION\_NEEDED</span>) but does not expose document follow-up state, reviewer notes, or rejection reasons. For full review status, check the dashboard.

## What's next

<CardGroup cols={2}>
  <Card title="Settlement models" icon="https://public-assets.openfx.com/docs/icons/settlement-models.svg" href="/v3/settlement-models">
    How balances, deposits, and withdrawals interact.
  </Card>

  <Card title="Settlement times" icon="https://public-assets.openfx.com/docs/icons/settlement-times.svg" href="/v3/settlement-times">
    Per-rail cut-offs for fiat withdrawal accounts.
  </Card>

  <Card title="Supported networks" icon="network-wired" href="/v3/supported-networks">
    Per-chain support for stablecoin withdrawal accounts.
  </Card>

  <Card title="Webhooks setup" icon="https://public-assets.openfx.com/docs/icons/webhooks-setup.svg" href="/v3/webhooks/setup">
    Receive `withdrawals` events for your withdrawal accounts.
  </Card>
</CardGroup>
