Skip to content
CoinsSendDevelopers
Payments documentation

Withdrawals API

View MarkdownAgent setup

This document describes the endpoints for initiating cryptocurrency withdrawals.

Important: To use this API, you must first enable the "Allow Withdrawals" setting in your merchant dashboard. Withdrawal requests will be rejected if this setting is not enabled.

Initiates a withdrawal of funds from your merchant account to an external wallet address.

POST /v1/withdrawals
NameRequiredDescription
Content-TypeYesMust be application/json
MerchantYesYour merchant ID
TimestampYesUnix timestamp in seconds. Must be within 5 minutes of server time. X-Timestamp is also accepted.
SignYesHMAC-SHA256 withdrawal signature (see Authentication)

Generate the canonical JSON body described in Authentication, sign it, and send that same string as the request body:

Sign = hmac_sha256(base64(canonical_json_body) + "." + Timestamp, API_KEY)
ParameterTypeRequiredDescription
networkstringYesBlockchain network (e.g., “tron”, “bsc”)
coinstringYesCryptocurrency (e.g., “usdt”, “usdc”)
amountstringYesAmount to withdraw in actual value (the minimum depends on the selected asset/network pair, e.g., “50.5” for 50.5 USDT)
to_addressstringYesDestination wallet address

Asset catalog: Use GET /v1/coins-and-fee to fetch the currently enabled coin/network pairs, network fees, and minimum withdrawal values. These values can change. The withdrawal endpoint remains the final validation authority for the pair and amount.

See Supported Coins & Networks for the coin/network tables and guidance on displaying withdrawal options.

{
"network": "tron",
"coin": "usdt",
"amount": "50.75",
"to_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE"
}
Note: Minimum and maximum withdrawal amounts depend on the current asset/network settings. Do not embed example thresholds as permanent constants.
{
"data": {
"success": true,
"withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"message": "Withdrawal request submitted successfully",
"status": "pending",
"amounts": {
"requested": {
"crypto": "100.00",
"usd": "100.00"
},
"net": {
"crypto": "98.15",
"usd": "98.15"
}
},
"fees": {
"merchant": {
"crypto": "0.45",
"usd": "0.45"
},
"network": {
"crypto": "1.40",
"usd": "1.40"
},
"total": {
"crypto": "1.85",
"usd": "1.85"
}
},
"fee_payer": {
"merchant_fee": false,
"network_fee": false
}
},
"status": "success"
}
FieldTypeDescription
dataobjectResponse data container
data.successbooleanWhether the withdrawal request succeeded
data.withdrawal_idstringUnique withdrawal identifier
data.messagestringHuman-readable message
data.statusstringCurrent withdrawal status (pending, processing, completed, failed)
data.amountsobjectRequested and net withdrawal amounts
data.amounts.requested.cryptostringRequested amount in cryptocurrency
data.amounts.requested.usdstringRequested amount in USD
data.amounts.net.cryptostringNet amount after fees in cryptocurrency
data.amounts.net.usdstringNet amount after fees in USD
data.feesobjectFee breakdown
data.fees.merchant.cryptostringMerchant fee in cryptocurrency
data.fees.merchant.usdstringMerchant fee in USD
data.fees.network.cryptostringNetwork fee in cryptocurrency
data.fees.network.usdstringNetwork fee in USD
data.fees.total.cryptostringTotal fee in cryptocurrency
data.fees.total.usdstringTotal fee in USD
data.fee_payer.merchant_feebooleanWhether merchant pays the merchant fee
data.fee_payer.network_feebooleanWhether merchant pays the network fee
statusstringOverall response status (“success” or “error”)

If your merchant account has the “Merchant Pays” settings enabled, the fields in fee_payer would be true and the amounts.net.crypto would equal the requested amounts.requested.crypto.

HTTP statusResponse shapeDescription
400{"error":"Missing headers"} or {"error":"Missing timestamp"}Required authentication input is missing
400endpoint error with status and messageIncludes withdrawal-permission denial and balance/fee rejection
401authentication {"error":"..."}Invalid merchant, signature, or expired timestamp
403authentication or endpoint errorMerchant status blocks withdrawals
422validation error with errors, status, and messagePair, amount, or destination validation failed
{
"status": "error",
"message": "Insufficient available balance"
}

Each asset/network pair has current minimum and maximum amounts. Fetch the current discovery catalog immediately before presenting withdrawal choices and handle 422 as the authoritative answer if configuration changes between discovery and submission. The public schema intentionally does not publish a closed asset enum or static limit table.

To use the Withdrawals API, you must first enable withdrawals in your merchant settings by checking the “Allow Withdrawals” checkbox. The description in settings reads: “Enable this to allow withdrawals via API”. This setting is required before any withdrawal API calls will work.

Additionally, there are two important settings that control how withdrawal fees are handled:

  1. “Pay Withdrawal Network Fee” - The description in settings reads: “Enable this if merchant will pay withdrawal network fee”. When enabled, the network fee is deducted from the merchant’s balance instead of the withdrawal amount.

  2. “Pay Withdrawal Service Fee” - The description in settings reads: “Enable this if merchant will pay withdrawal service fee”. When enabled, the merchant fee is deducted from the merchant’s balance instead of the withdrawal amount.

If both checkboxes are enabled, the recipient will receive exactly the amount specified in the API request, as all fees will be deducted from your merchant balance instead of the withdrawal amount.

When initiating withdrawals, two types of fees apply:

  1. Network Fee: A fixed fee for processing transactions on the blockchain
  2. Merchant Fee: A percentage-based fee applied to the withdrawal amount

Use GET /v1/merchants/fees for merchant-specific percentages and fee-payer settings, and GET /v1/coins-and-fee for current network-fee discovery. The accepted withdrawal response is the authoritative fee snapshot for that withdrawal.

The following arithmetic is illustrative only; substitute live fee values.

Scenario 1: Recipient Amount Is Reduced by Fees

When both “Pay Withdrawal Service Fee” and “Pay Withdrawal Network Fee” checkboxes are disabled:

Withdrawal amount: 100 USDT
Network fee: 1.4 USDT (fixed)
Merchant fee: 100 × 0.45% = 0.45 USDT
Total fees: 1.4 + 0.45 = 1.85 USDT
Amount sent to destination: 100 - 1.85 = 98.15 USDT
Deducted from merchant balance: 0 USDT

Scenario 2: Merchant Pays All Fees

When both “Pay Withdrawal Service Fee” and “Pay Withdrawal Network Fee” checkboxes are enabled:

Withdrawal amount: 100 USDT
Network fee: 1.4 USDT (fixed)
Merchant fee: 100 × 0.45% = 0.45 USDT
Total fees: 1.4 + 0.45 = 1.85 USDT
Amount sent to destination: 100 USDT (full amount)
Deducted from merchant balance: 1.85 USDT (all fees)

The response includes the fields fee_payer.merchant_fee and fee_payer.network_fee to indicate which fee payment options are active for your merchant account. These correspond directly to the “Pay Withdrawal Service Fee” and “Pay Withdrawal Network Fee” settings.

  • Withdrawals are processed asynchronously
  • The HTTP 200 response contains the current projected status, commonly pending or processing; it is not final settlement
  • You will receive withdrawal.in_progress, withdrawal.success, or withdrawal.failed webhooks
  • The to_address must be valid for the selected network
  • Always double-check the destination address before initiating a withdrawal
  • There is no request Idempotency-Key. If the outcome is unknown, reconcile by the returned/stored withdrawal ID or contact support instead of automatically submitting the same withdrawal again.

Generate withdrawal signatures only in trusted server-side code. Do not expose merchant API keys in browsers, mobile apps, or public frontend bundles.

<?php
// API credentials
$merchantId = 'your_merchant_id';
$apiKey = 'your_api_key';
// Withdrawal data
$withdrawalData = [
'network' => 'tron',
'coin' => 'usdt',
'amount' => '50.75',
'to_address' => 'TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE'
];
// Generate signature
$jsonData = json_encode($withdrawalData);
$base64Data = base64_encode($jsonData);
$timestamp = time();
$signature = hash_hmac('sha256', $base64Data . '.' . $timestamp, $apiKey);
// Set up request
$ch = curl_init('https://api.coinssend.com/v1/withdrawals');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Merchant: ' . $merchantId,
'Timestamp: ' . $timestamp,
'Sign: ' . $signature
]);
// Execute request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Process response
if ($httpCode >= 200 && $httpCode < 300) {
$responseData = json_decode($response, true);
if ($responseData['data']['success'] === true) {
echo "Withdrawal initiated successfully!\n";
echo "Withdrawal ID: " . $responseData['data']['withdrawal_id'] . "\n";
echo "Status: " . $responseData['data']['status'] . "\n";
echo "Message: " . $responseData['data']['message'] . "\n";
}
} else {
$errorData = json_decode($response, true);
echo "Error: " . ($errorData['error'] ?? $errorData['message'] ?? 'Unknown error') . "\n";
}
// API credentials
const merchantId = 'your_merchant_id';
const apiKey = 'your_api_key';
// Withdrawal data
const withdrawalData = {
'network': 'tron',
'coin': 'usdt',
'amount': '50.75',
'to_address': 'TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE'
};
const crypto = require('node:crypto');
// Generate signature
const jsonData = JSON.stringify(withdrawalData)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
);
const base64Data = Buffer.from(jsonData).toString('base64');
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', apiKey)
.update(`${base64Data}.${timestamp}`)
.digest('hex');
// Make the API request
fetch('https://api.coinssend.com/v1/withdrawals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Merchant': merchantId,
'Timestamp': timestamp,
'Sign': signature
},
body: jsonData
})
.then(response => {
if (!response.ok) {
return response.json().then(errorData => {
throw new Error(errorData.error || errorData.message || `HTTP error! status: ${response.status}`);
});
}
return response.json();
})
.then(data => {
if (data.data.success) {
console.log("Withdrawal initiated successfully!");
console.log(`Withdrawal ID: ${data.data.withdrawal_id}`);
console.log(`Status: ${data.data.status}`);
console.log(`Message: ${data.data.message}`);
}
})
.catch(error => {
console.error('Withdrawal error:', error.message);
});
import requests
import json
import base64
import hashlib
import hmac
import time
# API credentials
merchant_id = 'your_merchant_id'
api_key = 'your_api_key'
# Withdrawal data
withdrawal_data = {
'network': 'tron',
'coin': 'usdt',
'amount': '50.75',
'to_address': 'TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE'
}
def canonical_json(payload):
encoded = json.dumps(
payload, ensure_ascii=False, separators=(',', ':'), allow_nan=False
)
escaped = []
for character in encoded:
codepoint = ord(character)
if character == '/':
escaped.append(r'\/')
elif codepoint < 0x80:
escaped.append(character)
elif codepoint <= 0xFFFF:
escaped.append(f'\\u{codepoint:04x}')
else:
codepoint -= 0x10000
escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
return ''.join(escaped)
# Generate signature from the exact body that will be sent
json_data = canonical_json(withdrawal_data)
base64_data = base64.b64encode(json_data.encode()).decode()
timestamp = str(int(time.time()))
signature = hmac.new(
api_key.encode(),
f'{base64_data}.{timestamp}'.encode(),
hashlib.sha256
).hexdigest()
# Set up headers
headers = {
'Content-Type': 'application/json',
'Merchant': merchant_id,
'Timestamp': timestamp,
'Sign': signature
}
# Make the API request
try:
response = requests.post(
'https://api.coinssend.com/v1/withdrawals',
headers=headers,
data=json_data
)
# Check for HTTP errors
response.raise_for_status()
# Parse the response
response_data = response.json()
if response_data.get('data', {}).get('success'):
print("Withdrawal initiated successfully!")
print(f"Withdrawal ID: {response_data['data']['withdrawal_id']}")
print(f"Status: {response_data['data']['status']}")
print(f"Message: {response_data['data']['message']}")
else:
print(f"Error: {response_data.get('message', 'Unknown error')}")
except requests.exceptions.HTTPError as e:
error_data = {}
try:
error_data = response.json()
except:
pass
error_message = error_data.get('error') or error_data.get('message') or str(e)
print(f"HTTP Error: {error_message}")
except requests.exceptions.RequestException as e:
print(f"Request Error: {str(e)}")

For more details on working with withdrawals, see the Node.js withdrawal example.