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
Business API key for server-to-server integrations.
Key prefix determines the environment — no separate flag needed:
test_sk_* = sandbox/testnet, live_sk_* = production/mainnet.
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"
Path Parameters
Body
Optional. The fee_quote_hash from POST /api/v1/wallet/{customerID}/transfer-quote. If your fee configuration changed since that quote, the transfer is refused with 409 and code: "quote_changed" instead of sending at a fee your customer never saw.
Response
Transfer submitted to the network
Amount ACTUALLY delivered to the recipient, in base units — after every deduction. This is gross minus your merchant WaaS fee, and minus the TRON sponsorship fee on sponsored TRON USDT transfers. Always read this rather than echoing the amount you requested.
"USDT"
Fee collected in base units (omitted if no fee configured)
"1000000"
Hash of the fee collection transaction (omitted if no fee). Empty string for UTXO chains (BTC/LTC/BCH) where the fee is part of the main transaction.
Sender address (derived customer wallet address)
Network gas fee paid in base units (native token)
"ETH"
"pending"
Recipient address
Main transaction hash
"0xabc123..."

