> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs-v3.openfx.dev/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 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">
    Open [app.openfx.com](https://app.openfx.com) with an admin user. 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/etc 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 Fed Wire 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 (`PENDING`, `ACTIVE`, `ARCHIVED`, `REJECTED`, `DEACTIVATED`, `ADDITIONAL_ACTION_NEEDED`) by default; pass `?status=ACTIVE` (or filter client-side) based on your flow. Only `ACTIVE` accounts accept withdrawals — submitting against any other status returns an error.

    <Note>
      `GET /v3/fx/withdrawal-accounts` supports `?status=` (plus `?assetType=`, `?currency=`, `?network=`, and `?verified=`) as server-side filters. Filter to `ACTIVE` — 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={null}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from /v3/setup/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 withdrawal account for a currency + rail. For a CRYPTO
  // account, `rail` is the chain name (e.g. "SOLANA"). Kept separate from
  // the network call so it's testable without a real request.
  function findAccount(accounts, currency, rail) {
    return accounts.find((a) => a.currency === currency && a.rail === rail);
  }
  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={null}
  """Reads verified withdrawal accounts and picks the right one for a
  withdrawal — the pattern from
  /v3/setup/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, rail):
      """Picks the withdrawal account for a currency + rail. For a CRYPTO
      account, `rail` is the chain name (e.g. "SOLANA"). 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["rail"] == rail), 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={null}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/setup/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 rail;
          Account(String id, String currency, String rail) {
              this.id = id;
              this.currency = currency;
              this.rail = rail;
          }
      }
      // Picks the withdrawal account for a currency + rail. For a CRYPTO
      // account, rail is the chain name (e.g. "SOLANA"). Kept separate from
      // the network call so it's testable without a real request.
      static Account findAccount(List<Account> accounts, String currency, String rail) {
          for (Account a : accounts) {
              if (a.currency.equals(currency) && a.rail.equals(rail)) 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={null}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/setup/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 rail;
  };
  // Picks the withdrawal account for a currency + rail. For a CRYPTO
  // account, rail is the chain name (e.g. "SOLANA"). 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& rail) {
      for (const auto& a : accounts) {
          if (a.currency == currency && a.rail == rail) 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={null}
  // Reads verified withdrawal accounts and picks the right one for a
  // withdrawal — the pattern from
  // /v3/setup/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"`
  	Rail     string `json:"rail"`
  }
  // Picks the withdrawal account for a currency + rail. For a CRYPTO
  // account, Rail is the chain name (e.g. "SOLANA"). Kept separate from the
  // network call so it's testable without a real request.
  func findAccount(accounts []Account, currency, rail string) *Account {
  	for _, a := range accounts {
  		if a.Currency == currency && a.Rail == rail {
  			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={null}
  #!/usr/bin/env bash
  # Reads verified withdrawal accounts and picks the right one for a
  # withdrawal — the pattern from
  # /v3/setup/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.
  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")
  # Picks the withdrawal account for a currency + rail. For a CRYPTO account,
  # the rail is the chain name (e.g. "SOLANA"). 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 .rail == "SOLANA")'
  ```
</CodeGroup>

A typical response — a fiat row and a stablecoin row — looks like:

```json theme={null}
{
  "data": [
    {
      "object": "withdrawalAccount",
      "id": "wac_NDqQ9LmcUASpnHR6CTvdkk",
      "assetType": "FIAT",
      "rail": "ACH",
      "displayName": "Acme USD Reserves",
      "currency": "USD",
      "status": "ACTIVE",
      "verified": true,
      "creatorId": "usr_7m4VsfRw4pGrS76WYj5tnx",
      "destination": {
        "accountName": "Acme Reserves LLC",
        "accountNumber": "1234564321",
        "bankName": "JPMorgan Chase"
      },
      "createdAt": "2026-05-19T10:00:00.000Z",
      "updatedAt": "2026-05-19T10:00:00.000Z"
    },
    {
      "object": "withdrawalAccount",
      "id": "wac_7yE2Xr4vTqK9mB3sN6pW1",
      "assetType": "CRYPTO",
      "rail": "SOLANA",
      "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
  }
}
```

`rail` determines the shape of `destination`: chain name for `CRYPTO` (`destination.address`), or one of `SWIFT`, `FEDWIRE`, `FPS`, or another local rail 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={null}
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.
* **Deactivating.** Deactivate in the dashboard; the `id` stops accepting withdrawals immediately once `status` reads `DEACTIVATED`. In-flight withdrawals against a now-deactivated account complete normally.
* **Re-enabling.** Re-enable the same `id` if it gets reactivated; the UUID is stable.
* **Removal.** Permanent deletion strips the account from the list (`status: "ARCHIVED"`). Re-adding the same withdrawal account later returns a **new** `id`.

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

* **Re-using a v2 withdrawal-address record by ID in v3.** v3's `withdrawal_accounts` is a unified resource that fold v2's two surfaces ([stablecoin wallets](/v2/api-reference/withdrawals/list-withdrawal-wallets) and [fiat accounts](/v2/api-reference/withdrawals/list-fiat-withdrawal-accounts)) into one. IDs are not portable. Re-list via `GET /v3/fx/withdrawal-accounts`.
* **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.** Fed Wire vs SWIFT, FPS vs CHAPS — these have different cut-offs, fee profiles, and value limits. The rail is fixed at account creation; pick it deliberately. See [Settlement times](/v3/settlement-times).
* **Polling the API for full review status.** The API surfaces a `status` (`PENDING`, `ACTIVE`, `ARCHIVED`, `REJECTED`, `DEACTIVATED`, `ADDITIONAL_ACTION_NEEDED`) 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="Trade Settlement" icon="wallet" href="/v3/trade-settlement">
    How balances, deposits, and withdrawals interact.
  </Card>

  <Card title="Settlement times" icon="clock" 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="webhook" href="/v3/webhooks/setup">
    Receive `withdrawals` events for your withdrawal accounts.
  </Card>
</CardGroup>
