Skip to main content

Overview

Withdrawals allow users to transfer cryptocurrency from their custodial internal wallets to external blockchain addresses. All withdrawals require the destination address to be whitelisted for security and compliance.

Withdrawal Flow

Withdrawing from Internal Wallets

Execute a withdrawal from an internal custodial wallet to a whitelisted address:
curl -X POST "https://api.example.com/v1/wallet/internal/withdraw" \
  -H "x-client-key: YOUR_PUBLIC_KEY" \
  -H "Authorization: Bearer USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100.50",
    "recipientAddrss": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    "recipientMemo": null,
    "sourceAddress": "0x0a4b21fa733e9aeaddbf070302a85c559de13c4d",
    "sourceMemo": null,
    "currency": "usdc"
  }'

Request Parameters

FieldRequiredDescription
amountYesAmount to withdraw (as string for precision)
recipientAddrssYesDestination address (must be whitelisted)
recipientMemoNoDestination memo/tag (required for XRP, Stellar, etc.)
sourceAddressYesSource wallet address (from GET /v1/wallet/internal)
sourceMemoNoSource wallet memo (if applicable)
currencyYesCurrency to withdraw
Note the intentional typo in the API: recipientAddrss (with double ‘s’). This is the actual field name in the current API version.
The recipientAddrss (destination address) must be whitelisted before withdrawal. Attempting to withdraw to a non-whitelisted address will fail with a validation error.

Getting Source Address Details

Before withdrawing, retrieve the source wallet address from the internal wallets list:
curl -X GET "https://api.example.com/v1/wallet/internal" \
  -H "x-client-key: YOUR_PUBLIC_KEY" \
  -H "Authorization: Bearer USER_ACCESS_TOKEN"
Use the address field as sourceAddress and addressMemo (if not null) as sourceMemo in withdrawal requests.

Network-Specific Considerations

EVM Networks (Ethereum, Linea)

  • Standard ERC-20 token withdrawal
  • No memo required
  • Gas fees paid from wallet balance
  • Typical confirmation: 5-15 minutes (Ethereum), 1-2 minutes (Linea)
{
  "amount": "100",
  "recipientAddrss": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
  "recipientMemo": null,
  "sourceAddress": "0x0a4b21fa733e9aeaddbf070302a85c559de13c4d",
  "sourceMemo": null,
  "currency": "usdc"
}

Solana

  • SPL token withdrawal
  • No memo required
  • Transaction fees paid by platform
  • Typical confirmation: 5-30 seconds
{
  "amount": "50",
  "recipientAddrss": "DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK",
  "recipientMemo": null,
  "sourceAddress": "DfKNsYfrCEHb7ScJkuMTtPTeDiyjmBBm9NMHnbR7uFHz",
  "sourceMemo": null,
  "currency": "usdc"
}

XRP Ledger

  • Destination tag (memo) required for exchange addresses
  • Native XRP transfer
  • Typical confirmation: 3-5 seconds
  • Minimum withdrawal: 20 XRP (reserve requirement)
{
  "amount": "50",
  "recipientAddrss": "rNxp4h8apvRis6mJf9Sh8C6iRxfrDWN7AA",
  "recipientMemo": "66",
  "sourceAddress": "rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY",
  "sourceMemo": "78",
  "currency": "xrp"
}

Transaction Tracking

After successful withdrawal, the API returns success: true. Transaction details appear in wallet history:
curl -X GET "https://api.example.com/v1/wallet/history?walletId=098aeb90&walletType=INTERNAL&walletCurrency=usdc&page=0" \
  -H "x-client-key: YOUR_PUBLIC_KEY" \
  -H "Authorization: Bearer USER_ACCESS_TOKEN"
The transaction history shows withdrawal events but does not include on-chain transaction hashes. To get the transaction hash for blockchain explorer tracking, implement additional transaction logging in your application.

Implementation Examples

Complete Withdrawal Flow

async function withdrawToWhitelist(userToken, currency, amount, recipientIndex = 0) {
  // 1. Get internal wallets
  const walletsResponse = await fetch(
    'https://api.example.com/v1/wallet/internal',
    {
      headers: {
        'x-client-key': 'YOUR_PUBLIC_KEY',
        'Authorization': `Bearer ${userToken}`
      }
    }
  );
  const wallets = await walletsResponse.json();

  // 2. Find source wallet
  const sourceWallet = wallets.find(w => w.currency === currency);
  if (!sourceWallet) {
    throw new Error(`No ${currency} wallet found`);
  }

  // 3. Check balance
  if (parseFloat(sourceWallet.balance) < parseFloat(amount)) {
    throw new Error('Insufficient balance');
  }

  // 4. Get whitelisted addresses
  const whitelistResponse = await fetch(
    `https://api.example.com/v1/wallet/whitelist?currency=${currency}`,
    {
      headers: {
        'x-client-key': 'YOUR_PUBLIC_KEY',
        'Authorization': `Bearer ${userToken}`
      }
    }
  );
  const whitelisted = await whitelistResponse.json();

  if (whitelisted.length === 0) {
    throw new Error('No whitelisted addresses. Please add one first.');
  }

  const recipient = whitelisted[recipientIndex];

  // 5. Execute withdrawal
  const withdrawResponse = await fetch(
    'https://api.example.com/v1/wallet/internal/withdraw',
    {
      method: 'POST',
      headers: {
        'x-client-key': 'YOUR_PUBLIC_KEY',
        'Authorization': `Bearer ${userToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        amount: amount.toString(),
        recipientAddrss: recipient.walletAddress,
        recipientMemo: recipient.walletMemo,
        sourceAddress: sourceWallet.address,
        sourceMemo: sourceWallet.addressMemo,
        currency: currency
      })
    }
  );

  if (!withdrawResponse.ok) {
    const error = await withdrawResponse.json();
    throw new Error(error.message);
  }

  return await withdrawResponse.json();
}

// Usage
try {
  const result = await withdrawToWhitelist(userToken, 'usdc', '100.50');
  console.log('Withdrawal successful:', result);
} catch (error) {
  console.error('Withdrawal failed:', error.message);
}

Withdrawal UI Component

import { useState, useEffect } from 'react';

function WithdrawalForm({ userToken, currency }) {
  const [wallets, setWallets] = useState([]);
  const [whitelisted, setWhitelisted] = useState([]);
  const [amount, setAmount] = useState('');
  const [selectedAddress, setSelectedAddress] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  useEffect(() => {
    fetchData();
  }, [currency]);

  const fetchData = async () => {
    const [walletsRes, whitelistRes] = await Promise.all([
      fetch('https://api.example.com/v1/wallet/internal', {
        headers: {
          'x-client-key': 'YOUR_PUBLIC_KEY',
          'Authorization': `Bearer ${userToken}`
        }
      }),
      fetch(`https://api.example.com/v1/wallet/whitelist?currency=${currency}`, {
        headers: {
          'x-client-key': 'YOUR_PUBLIC_KEY',
          'Authorization': `Bearer ${userToken}`
        }
      })
    ]);

    const walletsData = await walletsRes.json();
    const whitelistData = await whitelistRes.json();

    setWallets(walletsData.filter(w => w.currency === currency));
    setWhitelisted(whitelistData);
  };

  const handleWithdraw = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError('');

    try {
      const sourceWallet = wallets[0];
      const recipient = whitelisted.find(w => w.id === selectedAddress);

      if (parseFloat(amount) > parseFloat(sourceWallet.balance)) {
        throw new Error('Insufficient balance');
      }

      const response = await fetch(
        'https://api.example.com/v1/wallet/internal/withdraw',
        {
          method: 'POST',
          headers: {
            'x-client-key': 'YOUR_PUBLIC_KEY',
            'Authorization': `Bearer ${userToken}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            amount: amount,
            recipientAddrss: recipient.walletAddress,
            recipientMemo: recipient.walletMemo,
            sourceAddress: sourceWallet.address,
            sourceMemo: sourceWallet.addressMemo,
            currency: currency
          })
        }
      );

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.message);
      }

      alert('Withdrawal successful!');
      setAmount('');
      setSelectedAddress('');
      await fetchData();
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const sourceWallet = wallets[0];

  return (
    <div className="withdrawal-form">
      <h3>Withdraw {currency.toUpperCase()}</h3>

      {sourceWallet && (
        <div className="balance-display">
          <label>Available Balance</label>
          <span className="balance">
            {sourceWallet.balance} {currency.toUpperCase()}
          </span>
        </div>
      )}

      {whitelisted.length === 0 ? (
        <div className="warning">
          No whitelisted addresses. Please add one before withdrawing.
        </div>
      ) : (
        <form onSubmit={handleWithdraw}>
          <div className="form-field">
            <label>Amount</label>
            <input
              type="number"
              step="0.01"
              value={amount}
              onChange={(e) => setAmount(e.target.value)}
              placeholder="0.00"
              required
            />
          </div>

          <div className="form-field">
            <label>Destination Address</label>
            <select
              value={selectedAddress}
              onChange={(e) => setSelectedAddress(e.target.value)}
              required
            >
              <option value="">Select address...</option>
              {whitelisted.map(addr => (
                <option key={addr.id} value={addr.id}>
                  {addr.name} - {addr.walletAddress.slice(0, 10)}...
                  {addr.walletMemo && ` (Memo: ${addr.walletMemo})`}
                </option>
              ))}
            </select>
          </div>

          {error && <div className="error">{error}</div>}

          <button type="submit" disabled={loading || !amount || !selectedAddress}>
            {loading ? 'Processing...' : 'Withdraw'}
          </button>
        </form>
      )}
    </div>
  );
}

Error Handling

Insufficient Balance

{
  "error": "Insufficient balance",
  "code": "WALLET_INSUFFICIENT_BALANCE",
  "message": "Wallet balance is insufficient for this withdrawal"
}
Solution: Check wallet balance before withdrawal. Display available balance to user.

Address Not Whitelisted

{
  "error": "Address not whitelisted",
  "code": "WALLET_ADDRESS_NOT_WHITELISTED",
  "message": "Recipient address must be whitelisted before withdrawal"
}
Solution: Add the address to whitelist first using POST /v1/wallet/whitelist.

Invalid Source Address

{
  "error": "Invalid source address",
  "code": "WALLET_INVALID_SOURCE",
  "message": "Source wallet not found or does not belong to this user"
}
Solution: Verify the source address and memo match values from GET /v1/wallet/internal.

Missing Required Memo

{
  "error": "Memo required",
  "code": "WALLET_MEMO_REQUIRED",
  "message": "This network requires a recipient memo/destination tag"
}
Solution: Provide recipientMemo for XRP and similar networks that require destination tags.

Amount Below Minimum

{
  "error": "Amount too low",
  "code": "WALLET_AMOUNT_TOO_LOW",
  "message": "Withdrawal amount below minimum threshold"
}
Solution: Check network-specific minimum withdrawal amounts (e.g., XRP has a 20 XRP reserve requirement).

Best Practices

Validate Before Submission

Check wallet balance, address whitelist status, and required fields before submitting withdrawal requests to provide better user feedback.

Show Transaction Status

Display clear status indicators during withdrawal processing. Blockchain confirmations can take seconds to minutes depending on the network.

Confirm with Users

Always show a confirmation dialog before executing withdrawals, displaying the amount, destination address, and expected fees.

Handle Memos Carefully

For XRP and similar networks, prominently display the destination tag requirement and validate that it’s included in the withdrawal request.

Display Network Fees

Inform users about approximate network fees for different blockchains to help them choose cost-effective withdrawal options.

Implement Retry Logic

Network issues can cause temporary failures. Implement exponential backoff retry logic for transient errors.

Test with Small Amounts

Encourage users to test new whitelisted addresses with small withdrawal amounts before large transfers.

Security Considerations

Irreversible Transactions: Blockchain transactions cannot be reversed. Double-check all withdrawal details before submission.
Whitelist Verification: Only withdraw to addresses you control and have verified. The whitelist prevents unauthorized destinations but cannot prevent user error.
Rate Limiting: Implement rate limiting on withdrawal requests to prevent abuse and protect user accounts from rapid fund drainage attacks.
Two-Factor Authentication: Consider requiring 2FA or email confirmation for withdrawal requests, especially for large amounts.

US Environment

For US-based users, route withdrawal requests to the US environment:
curl -X POST "https://api.example.com/v1/wallet/internal/withdraw" \
  -H "x-client-key: YOUR_PUBLIC_KEY" \
  -H "Authorization: Bearer USER_ACCESS_TOKEN" \
  -H "x-us-env: true" \
  -H "Content-Type: application/json" \
  -d '{...}'

Next Steps