network, or building a UI that lets users pick which chain to receive funds on.
What you’ll learn. Which chains each stablecoin supports, the network value the API returns for each chain, and per-network request bodies for POST /v3/fx/withdrawals.
Networks by stablecoin
USDC
| Network | API value |
|---|---|
| Aptos | APTOS |
| Arbitrum | ARBITRUM |
| Avalanche | AVALANCHE |
| Base | BASE |
| Binance Smart Chain | BSC |
| Ethereum | ETHEREUM |
| Optimism | OPTIMISM |
| Polygon | POLYGON |
| Solana | SOLANA |
| Tron | TRON |
USDT
| Network | API value |
|---|---|
| Aptos | APTOS |
| Arbitrum | ARBITRUM |
| Avalanche | AVALANCHE |
| Base | BASE |
| Binance Smart Chain | BSC |
| Ethereum | ETHEREUM |
| Optimism | OPTIMISM |
| Polygon | POLYGON |
| Solana | SOLANA |
| Tron | TRON |
EURC
| Network | API value |
|---|---|
| Avalanche | AVALANCHE |
| Base | BASE |
| Ethereum | ETHEREUM |
| Solana | SOLANA |
USDC on Ethereum and USDC on Polygon are different ledgers. Even when two
EVM chains share the same address format (e.g.
0x… on Ethereum and Polygon),
tokens sent on Polygon settle on the Polygon ledger and aren’t visible on
Ethereum, and vice versa. On non-EVM chains (Solana, Tron, Aptos), the address
formats are entirely different too. Either way, the verified withdrawal
account pins both the currency and the network; OpenFX routes the withdrawal
using the account’s specified network, and the resulting Withdrawal reports
the chain at network.How the API exposes the network
POST /v3/fx/withdrawals: You don’t specify a network on the request. The withdrawal account (identified bywithdrawalAccountId) carries the network, and OpenFX infers it from there. See Trade Settlement.GET /v3/fx/withdrawals/{id},GET /v3/fx/deposits: the chain a stablecoin movement settled on lives atnetwork(e.g.ETHEREUM). A fiat movement carriesnetwork: nullinstead (assetType: "FIAT"explains why). See Deposit lifecycle and Withdrawal lifecycle.GET /v3/fx/withdrawal-accounts: Therailfield identifies which chain a stablecoin account targets. Use this to pick the rightwithdrawalAccountIdfor a given chain.
Examples by network
Each example is a completePOST /v3/fx/withdrawals request body. Pick the verified withdrawalAccountId whose currency and network match the chain you want to send on.
// POST /v3/fx/withdrawals — the request body shape is identical across
// every chain; only withdrawalAccountId/withdrawalAmount/currency change.
// The pattern from /v3/supported-networks#examples-by-network.
// signRequest is the real helper from /v3/authentication — adjust this path
// if you've copied it somewhere else in your own project.
const { randomUUID } = require("node:crypto");
const { signRequest } = require("../../authentication/node/sign-request");
// Builds the POST /v3/fx/withdrawals body. Kept separate from the network
// call so it's testable without a real request.
function buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency) {
return { withdrawalAccountId, withdrawalAmount, currency };
}
async function withdraw(withdrawalAccountId, withdrawalAmount, currency) {
const url = "https://api.openfx.com/v3/fx/withdrawals";
const body = JSON.stringify(buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency));
const { headers } = signRequest({ method: "POST", url, body });
headers["Idempotency-Key"] = randomUUID();
const res = await fetch(url, { method: "POST", headers, body });
return res.json();
}
module.exports = { buildWithdrawalBody, withdraw };
if (require.main === module) {
// USDC on Ethereum — see the table on /v3/supported-networks#examples-by-network
// for the withdrawalAccountId per network.
withdraw("8ff46b97-5742-4196-88dd-db7b0b46ab7a", "1000.00", "USDC").catch((err) => {
console.error("Error:", err.message);
process.exitCode = 1;
});
}
"""POST /v3/fx/withdrawals — the request body shape is identical across
every chain; only withdrawal_account_id/withdrawal_amount/currency change.
The pattern from /v3/supported-networks#examples-by-network."""
import json
import os
import sys
import uuid
# 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 build_withdrawal_body(withdrawal_account_id, withdrawal_amount, currency):
"""Builds the POST /v3/fx/withdrawals body. Kept separate from the
network call so it's testable without a real request."""
return {
"withdrawalAccountId": withdrawal_account_id,
"withdrawalAmount": withdrawal_amount,
"currency": currency,
}
def withdraw(withdrawal_account_id, withdrawal_amount, currency):
url = "https://api.openfx.com/v3/fx/withdrawals"
body = json.dumps(build_withdrawal_body(withdrawal_account_id, withdrawal_amount, currency)).encode("utf-8")
headers, _ = sign_request("POST", url, body)
headers["Idempotency-Key"] = str(uuid.uuid4())
return requests.post(url, headers=headers, data=body).json()
if __name__ == "__main__":
# USDC on Ethereum — see the table on
# /v3/supported-networks#examples-by-network for the
# withdrawal_account_id per network.
print(withdraw("8ff46b97-5742-4196-88dd-db7b0b46ab7a", "1000.00", "USDC"))
// POST /v3/fx/withdrawals — the request body shape is identical across
// every chain; only withdrawalAccountId/withdrawalAmount/currency change.
// The pattern from /v3/supported-networks#examples-by-network. Uses only
// the built-in JDK APIs — no external dependency, so JSON serialization
// below is intentionally minimal (a flat object serializer), not a
// general JSON library.
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
public class Withdraw {
// Builds the POST /v3/fx/withdrawals body. Kept separate from the
// network call so it's testable without a real request.
static Map<String, String> buildWithdrawalBody(String withdrawalAccountId, String withdrawalAmount, String currency) {
Map<String, String> body = new LinkedHashMap<>();
body.put("withdrawalAccountId", withdrawalAccountId);
body.put("withdrawalAmount", withdrawalAmount);
body.put("currency", currency);
return body;
}
// Escapes a string for embedding in a JSON string literal. Sufficient
// for this page's field values; not a general JSON serializer.
static String escapeJson(String value) {
StringBuilder out = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '"': out.append("\\\""); break;
case '\\': out.append("\\\\"); break;
case '\n': out.append("\\n"); break;
case '\r': out.append("\\r"); break;
case '\t': out.append("\\t"); break;
default:
if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
else out.append(c);
}
}
return out.toString();
}
// Serializes a flat string-valued map to a JSON object. Sufficient for
// this page's request body; not a general JSON serializer.
static String toJsonObject(Map<String, String> fields) {
StringBuilder json = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, String> entry : fields.entrySet()) {
if (!first) json.append(",");
first = false;
json.append("\"").append(entry.getKey()).append("\":\"").append(escapeJson(entry.getValue())).append("\"");
}
return json.append("}").toString();
}
static void withdraw(String withdrawalAccountId, String withdrawalAmount, String currency) throws Exception {
String url = "https://api.openfx.com/v3/fx/withdrawals";
byte[] body = toJsonObject(buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency))
.getBytes(StandardCharsets.UTF_8);
// 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("POST", url, body);
headers.put("Idempotency-Key", UUID.randomUUID().toString());
HttpURLConnection conn = (HttpURLConnection) URI.create(url).toURL().openConnection();
conn.setRequestMethod("POST");
for (Map.Entry<String, String> h : headers.entrySet()) {
conn.setRequestProperty(h.getKey(), h.getValue());
}
conn.setDoOutput(true);
conn.getOutputStream().write(body);
conn.getResponseCode();
}
public static void main(String[] args) throws Exception {
// USDC on Ethereum — see the table on
// /v3/supported-networks#examples-by-network for the
// withdrawalAccountId per network.
withdraw("8ff46b97-5742-4196-88dd-db7b0b46ab7a", "1000.00", "USDC");
}
}
// POST /v3/fx/withdrawals — the request body shape is identical across
// every chain; only withdrawalAccountId/withdrawalAmount/currency change.
// The pattern from /v3/supported-networks#examples-by-network. Requires
// OpenSSL and libcurl. No JSON library was added, so serialization below
// is intentionally minimal (a flat object serializer), not a general
// JSON library.
#include <curl/curl.h>
#include <openssl/rand.h>
#include <iomanip>
#include <map>
#include <sstream>
#include <string>
// 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);
// Builds the POST /v3/fx/withdrawals body. Kept separate from the network
// call so it's testable without a real request.
std::map<std::string, std::string> buildWithdrawalBody(const std::string& withdrawalAccountId,
const std::string& withdrawalAmount,
const std::string& currency) {
return {
{"withdrawalAccountId", withdrawalAccountId},
{"withdrawalAmount", withdrawalAmount},
{"currency", currency},
};
}
// Escapes a string for embedding in a JSON string literal. Sufficient for
// this page's field values; not a general JSON serializer.
std::string escapeJson(const std::string& value) {
std::ostringstream out;
for (unsigned char c : value) {
switch (c) {
case '"': out << "\\\""; break;
case '\\': out << "\\\\"; break;
case '\n': out << "\\n"; break;
case '\r': out << "\\r"; break;
case '\t': out << "\\t"; break;
default:
if (c < 0x20) out << "\\u" << std::hex << std::setfill('0') << std::setw(4) << (int)c << std::dec;
else out << (char)c;
}
}
return out.str();
}
// Serializes a flat string-valued map to a JSON object. Sufficient for
// this page's request body; not a general JSON serializer.
std::string toJsonObject(const std::map<std::string, std::string>& fields) {
std::ostringstream json;
json << "{";
bool first = true;
for (const auto& [key, value] : fields) {
if (!first) json << ",";
first = false;
json << "\"" << key << "\":\"" << escapeJson(value) << "\"";
}
json << "}";
return json.str();
}
// Generates a random per-call idempotency key. Not RFC 4122 UUID format —
// idempotency keys only need to be unique client-generated strings — but
// uses the same RAND_bytes source as the JWT nonce in sign_request.cpp.
std::string randomIdempotencyKey() {
unsigned char bytes[16];
RAND_bytes(bytes, sizeof(bytes));
std::ostringstream hex;
for (unsigned char b : bytes) hex << std::hex << std::setfill('0') << std::setw(2) << (int)b;
return hex.str();
}
static size_t discard(char*, size_t s, size_t n, void*) { return s * n; }
void withdraw(const std::string& withdrawalAccountId, const std::string& withdrawalAmount,
const std::string& currency) {
std::string url = "https://api.openfx.com/v3/fx/withdrawals";
std::string body = toJsonObject(buildWithdrawalBody(withdrawalAccountId, withdrawalAmount, currency));
SignedRequest req = signRequest("POST", "/v3/fx/withdrawals", "", body);
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());
headers = curl_slist_append(headers, ("Idempotency-Key: " + randomIdempotencyKey()).c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discard);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
#ifndef WITHDRAW_NO_MAIN
#include <iostream>
int main() {
try {
// USDC on Ethereum — see the table on
// /v3/supported-networks#examples-by-network for the
// withdrawalAccountId per network.
withdraw("8ff46b97-5742-4196-88dd-db7b0b46ab7a", "1000.00", "USDC");
return 0;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
}
#endif
// POST /v3/fx/withdrawals — the request body shape is identical across
// every chain; only WithdrawalAccountId/WithdrawalAmount/Currency change.
// The pattern from /v3/supported-networks#examples-by-network.
package main
import (
"bytes"
"crypto/rand"
"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 WithdrawalBody struct {
WithdrawalAccountID string `json:"withdrawalAccountId"`
WithdrawalAmount string `json:"withdrawalAmount"`
Currency string `json:"currency"`
}
// Builds the POST /v3/fx/withdrawals body. Kept separate from the network
// call so it's testable without a real request.
func buildWithdrawalBody(withdrawalAccountID, withdrawalAmount, currency string) WithdrawalBody {
return WithdrawalBody{WithdrawalAccountID: withdrawalAccountID, WithdrawalAmount: withdrawalAmount, Currency: currency}
}
func newIdempotencyKey() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}
func withdraw(withdrawalAccountID, withdrawalAmount, currency string) error {
url := "https://api.openfx.com/v3/fx/withdrawals"
body, err := json.Marshal(buildWithdrawalBody(withdrawalAccountID, withdrawalAmount, currency))
if err != nil {
return err
}
signed, err := auth.SignRequest("POST", url, body)
if err != nil {
return err
}
key, err := newIdempotencyKey()
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
if err != nil {
return err
}
for k, v := range signed.Headers {
req.Header.Set(k, v)
}
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
return nil
}
func main() {
// USDC on Ethereum — see the table on
// /v3/supported-networks#examples-by-network for the
// withdrawalAccountId per network.
if err := withdraw("8ff46b97-5742-4196-88dd-db7b0b46ab7a", "1000.00", "USDC"); err != nil {
fmt.Println("Error:", err)
}
}
#!/usr/bin/env bash
# POST /v3/fx/withdrawals — the request body shape is identical across
# every chain; only withdrawalAccountId/withdrawalAmount/currency change.
# The pattern from /v3/supported-networks#examples-by-network. 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
# USDC on Ethereum — see the table on
# /v3/supported-networks#examples-by-network for the withdrawalAccountId
# per network.
export OPENFX_JWT="<jwt-from-signRequest>"
export OPENFX_SIGNATURE="<signature-from-signRequest>"
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": "8ff46b97-5742-4196-88dd-db7b0b46ab7a",
"withdrawalAmount": "1000.00",
"currency": "USDC"
}'
The request body shape is identical across every chain — only
withdrawalAccountId, withdrawalAmount, and currency change. The network
is bound to the verified withdrawal account, so swapping chains is a matter of
selecting a different withdrawalAccountId from GET /v3/fx/withdrawal-accounts. The examples above use USDC on Ethereum; here’s
the same request for every other network on this page:| Network | currency | withdrawalAccountId | withdrawalAmount |
|---|---|---|---|
| USDC on Ethereum | USDC | 8ff46b97-5742-4196-88dd-db7b0b46ab7a | 1000.00 |
| USDC on Solana | USDC | a2c7d4f1-3b18-4f6a-9c35-2e8a1b4d9f02 | 1000.00 |
| USDT on Tron | USDT | b9e1d5a7-7c2f-49b8-8d04-4ac6e3f50d83 | 2500.00 |
| USDT on BSC | USDT | c4f8e2b0-5d63-4a17-bf90-1e7c3a92e441 | 500.00 |
| EURC on Base | EURC | d7a3b8c1-9f45-4e26-a812-6b4d5e90f378 | 1500.00 |
Common mistakes
- Sending to the wrong-chain address. A USDC withdrawal sent to a Polygon address using an Ethereum-network
withdrawalAccountIdwill not be recoverable. Always confirm the verified withdrawal account’srailmatches your intent before withdrawing. - Treating
USDCas one balance. YouravailableBalanceforUSDCspans all chains, but withdrawals are pinned to one chain via thewithdrawalAccountId. There is no “move from Ethereum USDC to Solana USDC” inside OpenFX. That requires a bridge. - Looking for a chain on a fiat resource. A fiat deposit or withdrawal carries
network: null(itsassetTypeis"FIAT") — there is no chain to read. Use Settlement Times for fiat-rail information.
What’s next
Trade Settlement
Deposits in, withdrawals out. The unified fiat + stablecoin endpoint.
Settlement times
Fiat-rail submission cut-offs by currency.
Withdrawal lifecycle
pending → processing → completed state machine.
Deposit lifecycle
How deposits are detected and credited.