curl --request POST \
--url https://api.rach.finance/api/v1/wallet/{customerID}/transfer \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"fee_quote_hash": "<string>"
}
'import requests
url = "https://api.rach.finance/api/v1/wallet/{customerID}/transfer"
payload = { "fee_quote_hash": "<string>" }
headers = {
"Idempotency-Key": "<idempotency-key>",
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
'X-API-Key': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({fee_quote_hash: '<string>'})
};
fetch('https://api.rach.finance/api/v1/wallet/{customerID}/transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.rach.finance/api/v1/wallet/{customerID}/transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'fee_quote_hash' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.rach.finance/api/v1/wallet/{customerID}/transfer"
payload := strings.NewReader("{\n \"fee_quote_hash\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.rach.finance/api/v1/wallet/{customerID}/transfer")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"fee_quote_hash\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rach.finance/api/v1/wallet/{customerID}/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fee_quote_hash\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"amount": "<string>",
"currency": "USDT",
"fee_amount": "1000000",
"fee_tx_hash": "<string>",
"from_address": "<string>",
"gas_fee": "<string>",
"network": "ETH",
"status": "pending",
"timestamp": "2023-11-07T05:31:56Z",
"to_address": "<string>",
"tx_hash": "0xabc123..."
}{
"error": "USDT is not supported on Solana — use USDT on ETH, BSC, POL, or TRX"
}Send crypto from customer wallet
Broadcasts a signed blockchain transaction from the customer’s derived address at the given index.
amount format (default): Pass the value as a JSON string representing a human-readable
coin amount. A bare integer and a decimal mean the same thing — whole coins:
"2.5"on ETH → 2.5 ETH"100"on USDT → 100 USDT"0.001"on BTC → 0.001 BTC
⚠️ Breaking change (2026-07): a bare integer like
"5"is now 5 whole coins (e.g. 5 BTC), NOT raw base units. Integrations that previously sent pre-converted base units (wei/lamports/satoshis/drops) as a bare integer must now set"unit": "base"(below), or send a decimal string. This removes the prior footgun where"100"USDT silently meant dust.
Base units (opt-in): set "unit": "base" to pass a pre-converted positive integer in the
smallest unit (wei/satoshi/lamport/drop); it is used as-is. A decimal point is rejected in this mode.
Decimal precision per network/currency (used for decimal→base conversion):
| Network | Currency | Decimals | Example: 1 unit in base units |
|---|---|---|---|
| ETH | ETH | 18 | "1000000000000000000" |
| ETH | USDT, USDC | 6 | "1000000" |
| BSC | BNB | 18 | "1000000000000000000" |
| BSC | USDT, USDC | 18 | "1000000000000000000" |
| POL | POL | 18 | "1000000000000000000" |
| POL | USDT, USDC | 6 | "1000000" |
| TRX | TRX, USDT | 6 | "1000000" |
| SOL | SOL | 9 | "1000000000" |
| SOL | USDC | 6 | "1000000" |
| BTC, LTC, BCH | native | 8 | "100000000" |
| BASE, ARB | ETH | 18 | "1000000000000000000" |
| BASE, ARB | USDT, USDC | 6 | "1000000" |
| AVAX | AVAX | 18 | "1000000000000000000" |
| CELO | CELO | 18 | "1000000000000000000" |
| AVAX, CELO | USDT, USDC | 6 | "1000000" |
| SOL | USDT | 6 | "1000000" |
| XRP | XRP | 6 | "1000000" |
BSC USDT/USDC = 18 decimals (Binance-pegged). POL USDT/USDC = 6 decimals (Polygon PoS bridged tokens).
XRP note: for XRP transfers you may pass destination_tag (integer). Most exchanges require
a destination tag to credit XRP deposits — omitting it can make the funds uncreditable. The server
also rejects a first payment to an unactivated destination below the ~1 XRP base reserve.
Requires wallet:transfer permission.
Blocked combinations — returns 400:
TRX+USDC— USDC does not exist as a TRC-20 token on Tron
USDT is supported on Solana (SPL mint Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB).
Call GET /api/v1/wallet/assets for the authoritative list in your environment rather than
hard-coding pairs.
Transfers require a live key. In test mode this endpoint returns 400; wallet creation, address derivation and balance reads work on a test key, but sending does not.
Fee collection: If the merchant has configured a WaaS fee (via POST /api/v1/wallet/fees),
the fee is automatically deducted from the transfer amount and sent on-chain to the merchant’s
configured address. The response includes fee_amount and fee_tx_hash when a fee was collected.
For UTXO chains (BTC/LTC/BCH) the fee is a third output in the same transaction (fee_tx_hash is empty).
For all other chains the fee is a second transaction.
TRON USDT — sponsored gas, 1 USDT deducted. A TRON address cannot send until it has been activated on-chain, and a TRC-20 transfer costs ENERGY that a customer holding only USDT does not have. Rach activates the address and covers the energy automatically, so the transfer succeeds with zero TRX in the wallet. To recover that cost, 1 USDT (configurable) is deducted from the transfer amount and sent to the platform sponsor wallet:
- Taken out of
amount, not charged separately — a customer whose whole balance is USDT can still transfer. - Charged only after the customer transfer succeeds.
- Skipped when the amount is too small to carry it.
- Applies only to USDT on TRX, and only when Rach actually sponsored the transfer. A customer with their own energy pays nothing.
- This is separate from, and additional to, any merchant WaaS fee you configure.
Always read
amountin the response for the figure actually delivered to the recipient — it reflects both the merchant fee and the TRON sponsorship fee.
curl --request POST \
--url https://api.rach.finance/api/v1/wallet/{customerID}/transfer \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--header 'X-API-Key: <api-key>' \
--data '
{
"fee_quote_hash": "<string>"
}
'import requests
url = "https://api.rach.finance/api/v1/wallet/{customerID}/transfer"
payload = { "fee_quote_hash": "<string>" }
headers = {
"Idempotency-Key": "<idempotency-key>",
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
'X-API-Key': '<api-key>',
'Content-Type': 'application/json'
},
body: JSON.stringify({fee_quote_hash: '<string>'})
};
fetch('https://api.rach.finance/api/v1/wallet/{customerID}/transfer', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.rach.finance/api/v1/wallet/{customerID}/transfer",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'fee_quote_hash' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Idempotency-Key: <idempotency-key>",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.rach.finance/api/v1/wallet/{customerID}/transfer"
payload := strings.NewReader("{\n \"fee_quote_hash\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.rach.finance/api/v1/wallet/{customerID}/transfer")
.header("Idempotency-Key", "<idempotency-key>")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"fee_quote_hash\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rach.finance/api/v1/wallet/{customerID}/transfer")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"fee_quote_hash\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"amount": "<string>",
"currency": "USDT",
"fee_amount": "1000000",
"fee_tx_hash": "<string>",
"from_address": "<string>",
"gas_fee": "<string>",
"network": "ETH",
"status": "pending",
"timestamp": "2023-11-07T05:31:56Z",
"to_address": "<string>",
"tx_hash": "0xabc123..."
}{
"error": "USDT is not supported on Solana — use USDT on ETH, BSC, POL, or TRX"
}Authorizations
Headers
Required, 16-128 characters. Reuse the same value to retry a transfer safely: a replay returns the original transfer instead of sending a second one. Read the outcome back with GET /api/v1/wallet/{customerID}/transfers/{key}. A request without this header is rejected with 400.
16 - 128"8f14e45f-ea6d-4f3a-9c21-7b0e5d3a1c92"
Body
Response
Transfer submitted to the network

