Withdrawals

Last updated: 2026-10-22

The withdrawals API allows you to process payouts to customers and external addresses. This guide covers withdrawal creation, status tracking, balance validation, and best practices for managing withdrawal flows.

Overview

The withdrawal flow consists of these steps:

  1. Validate balance - Ensure sufficient funds are available on the chain you are withdrawing from
  2. Create withdrawal - Submit withdrawal request with destination address and network
  3. Monitor status - Track withdrawal processing via webhooks and API calls
  4. Handle confirmations - Process completed withdrawals in your system

Supported currencies and networks

Balances are segmented per (currency, network) pair. There is no cross-chain fungibility: a withdrawal draws only from the balance on the same network.

CurrencyNetworkStatus
USDTTRX (Tron)Available
USDTTRX_NILE (Tron Nile testnet)Available (staging/testing)
USDT / USDCBNB (BNB Smart Chain)Deposits live since 2026-09-08; withdrawals enabled after the first production deposits (see the Changelog)
USDT / USDCPOL (Polygon PoS)Deposits live since 2026-09-09; withdrawals enabled after the first production deposits
USDT / USDCETH, SOLComing soon / on request

Requests referencing a disabled (currency, network) pair are rejected with 400 and an error such as "Withdrawals of USDT on ETH are not supported".

Creating Withdrawals

POST /api/withdrawals

Create a new withdrawal request to send funds to a destination address.

Request

curl -X POST "https://api.dcepay.io/api/withdrawals" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "100.50",
    "currency": "USDT",
    "network": "TRX",
    "destination": "TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
    "description": "Payout for order 123",
    "referenceId": "order_123"
  }'

Request Parameters

ParameterTypeRequiredDescription
amountstringYesAmount to withdraw as a positive decimal string (up to 18 decimal places), e.g. "100.50"
currencystringYesToken symbol: USDT or USDC
networkstringYesNetwork symbol: TRX, ETH, BNB, POL, SOL (testnets: TRX_NILE, SEP, tBNB, POL_AMOY, SOL_DEVNET). Only pairs with withdrawals enabled are accepted — see Supported currencies and networks above
destinationstringYesDestination address for the withdrawal
descriptionstringNoFree-text description for the withdrawal
referenceIdstringNoYour own reference for this withdrawal. Unique per merchant — reusing a referenceId never creates a second payout (see Idempotency)

Response

{
  "success": true,
  "message": "Withdrawal request received and will begin processing",
  "withdrawalId": "cmdl8u2xq0001abcd1234efgh",
  "status": "PENDING",
  "amount": "100.5",
  "currency": "USDT",
  "network": "TRX",
  "destination": "TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
  "feeInfo": {
    "grossAmount": "100.5",
    "netAmount": "99.3",
    "totalFees": "1.2",
    "feeBreakdown": {
      "baseFee": "0",
      "markupRate": "0.001",
      "markupAmount": "0",
      "networkFee": "1.2",
      "totalFee": "1.2"
    }
  }
}

Important: fees are charged on top of the withdrawal amount. The destination address receives the full amount; your balance is debited amount + max(commission, networkFee). Make sure your available balance covers the total, not just the amount.

JavaScript Example

async function createWithdrawal(currency, network, amount, destination, referenceId) {
  const response = await fetch('https://api.dcepay.io/api/withdrawals', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.DCE_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      currency,
      network,
      amount,     // decimal string, e.g. "100.50"
      destination,
      referenceId
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Withdrawal creation failed: ${error.message || error.error}`);
  }

  return response.json();
}

// Usage
const withdrawal = await createWithdrawal('USDT', 'TRX', '100.50',
  'TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5', 'order_123');

console.log('Withdrawal ID:', withdrawal.withdrawalId);
console.log('Total fees (charged on top):', withdrawal.feeInfo.totalFees);

Listing Withdrawals

GET /api/withdrawals

Retrieve a list of your withdrawals with optional filtering.

Request

curl -X GET "https://api.dcepay.io/api/withdrawals?status=CONFIRMED&page=1&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Query Parameters

ParameterTypeDescription
statusstringFilter by status (PENDING, CONFIRMED, FAILED, CANCELLED)
pagenumberPage number (default: 1)
limitnumberNumber of results per page (default: 10, max: 100)

Response

{
  "withdrawals": [
    {
      "id": "cmdl8u2xq0001abcd1234efgh",
      "amount": "100.5",
      "currency": "USDT",
      "status": "CONFIRMED",
      "createdAt": "2026-07-19T10:30:00Z",
      "destination": "TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
      "processedAt": "2026-07-19T10:31:12Z",
      "network": "TRX",
      "description": "Payout for order 123",
      "fee": "0",
      "commission": "1",
      "commissionRate": "0"
    }
  ],
  "pagination": {
    "total": 75,
    "page": 1,
    "limit": 10,
    "pages": 8
  }
}

Balance Validation

Before creating a withdrawal, always check your available balance on the network you are withdrawing from:

GET /api/balance

curl -X GET "https://api.dcepay.io/api/balance" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

Balances are returned per (currency, network) pair, together with the current network fee and the maximum withdrawable amount after fees:

{
  "balances": [
    {
      "currency": "USDT",
      "network": "TRX",
      "available": "250.75",
      "pending": "10",
      "withdrawalEnabled": true,
      "networkFee": "1.2",
      "maxWithdrawable": "249.55",
      "lastUpdatedAt": "2026-07-19T10:30:00Z"
    }
  ]
}

You can also query a single pair with ?currency=USDT&network=TRX, which returns one object (currency, network, available, pending, lastUpdatedAt).

Balance Validation Example

async function validateWithdrawalBalance(currency, network, amount) {
  const response = await fetch('https://api.dcepay.io/api/balance', {
    headers: {
      'Authorization': `Bearer ${process.env.DCE_API_KEY}`
    }
  });

  const { balances } = await response.json();
  const balance = balances.find(b => b.currency === currency && b.network === network);

  if (!balance) {
    throw new Error(`No ${currency} balance on ${network}`);
  }

  // Remember: total debit = amount + max(commission, networkFee)
  if (parseFloat(balance.maxWithdrawable) < parseFloat(amount)) {
    throw new Error(`Insufficient balance on ${network}. Max withdrawable: ${balance.maxWithdrawable} ${currency}`);
  }

  return true;
}

Withdrawal Status Tracking

Status Values

StatusDescription
PENDINGWithdrawal created and submitted, funds reserved, waiting for on-chain confirmation
CONFIRMEDWithdrawal completed and confirmed on the blockchain
FAILEDWithdrawal failed — the reserved amount (including fees) is returned to your balance
CANCELLEDWithdrawal was cancelled

Status Transitions

PENDING → CONFIRMED (when the payout is submitted and confirmed on-chain)
PENDING → FAILED (if payout initiation or on-chain processing fails)

When a withdrawal fails, the full reserved amount (amount plus all fees) is released back to your available balance, and your referenceId is freed for reuse on a retry.

Fee Structure

Withdrawal fees are charged on top of the withdrawal amount. Every withdrawal is debited:

total debit = amount + max(commission, networkFee)
  • Commission — your merchant withdrawal fee: 0.1% of the amount for all merchants since 2026-09-03 (a flat per-network fee in token units for accounts configured that way).
  • Network fee — a per-(currency, network) fee quoted at submission time. On TRX: 1.20 USDT standard, or 2.50 USDT when the destination address holds no USDT at submission (such transfers cost about twice the network resources; if the destination balance cannot be read, the higher tier is quoted). The fee quoted at submission is what you pay, even if the finalized on-chain cost differs.
  • The network fee is a floor under the commission (since 2026-09-01), not an add-on: the reported commission is only the margin above the network fee, and is 0 whenever amount × 0.1% is below it (amounts under 1,200 USDT at the standard tier).

Effective 2026-09-03 00:00 GMT+8. Before that: 1.00 USDT network fee, flat commissions stacked on top until 2026-09-01.

Per-network flat fees

Default flat commission per network (token units) — your account may have custom values:

NetworkDefault flat fee
TRX1
ETH2
BNB0.2
POL0.2
SOL0.2

Testnets mirror their mainnet fee.

Fee Calculation

// For a 100 USDT withdrawal on TRX at the 0.1% rate, standard tier:
const amount = 100;
const commission = amount * 0.001;   // 0.10
const networkFee = 1.2;              // 2.5 if the destination holds no USDT
const totalFee = Math.max(commission, networkFee); // 1.20
const totalDebit = amount + totalFee;              // 101.20 USDT debited
// Destination receives the full 100 USDT

// For 5,000 USDT: commission 5.00 > networkFee 1.20 → totalFee 5.00,
// reported as commission 3.80 (margin) + networkFee 1.20.

System Configuration

Withdrawal fees are configured internally by system administrators and cannot be modified via the merchant API. The fee resolution order is:

  1. Merchant percentage rate — if configured, commission = amount × rate (clamped to your account's min/max charge limits)
  2. Merchant per-network flat fee — flat fee in token units for the specific (currency, network) pair
  3. Merchant legacy flat fee — single flat fee if no per-network fee is set
  4. System defaults — system percentage or system base fee

The resolved commission is then floored by the network fee as described above. A minimum commission of 0.10 USDT applies inside the resolver (unless withdrawal fees are disabled system-wide), but in practice the network fee is the binding floor.

Admin API for Fee Management

Administrators can manage system-level withdrawal fee settings:

Get Current Settings:

GET /api/admin/withdrawal-fees
Authorization: Bearer <admin-api-key>

Update Settings:

POST /api/admin/withdrawal-fees
Authorization: Bearer <admin-api-key>
Content-Type: application/json

{
  "baseFee": "1.5",
  "markupRate": "0.02",
  "enabled": true
}

Fee Information in Response

The withdrawal API response includes detailed fee information in feeInfo:

{
  "feeInfo": {
    "grossAmount": "100",
    "netAmount": "98.8",
    "totalFees": "1.2",
    "feeBreakdown": {
      "baseFee": "0",
      "markupRate": "0.001",
      "markupAmount": "0",
      "networkFee": "1.2",
      "totalFee": "1.2"
    }
  }
}
FieldDescription
feeBreakdown.baseFee / markupAmountThe commission margin above the network fee (0 when your commission does not exceed it)
feeBreakdown.markupRateThe percentage rate applied (0 when the commission is flat)
feeBreakdown.networkFeeThe per-network fee quoted at submission (standard or fresh-destination tier)
feeBreakdown.totalFee / totalFeesmax(commission, networkFee) = baseFee + networkFee — charged on top of amount

Reseller Payouts

If your API key belongs to a reseller account, withdrawals behave differently:

  • The destination is pinned to your registered withdrawal address — a mismatched destination or network is rejected with 400.
  • The payout requires admin approval: funds are reserved immediately and the response returns "status": "PENDING_APPROVAL" with the message "Withdrawal request received and is awaiting admin approval". The on-chain payout is submitted only after approval.

Webhook Notifications

You'll receive webhook notifications when withdrawal status changes. Delivery is at-least-once (durable outbox with retries) — deduplicate using the eventId field, and acknowledge with a JSON body of { "ok": true }. The event type is carried in the X-Webhook-Event header.

The referenceId (and identifier) in callback payloads is your own merchant referenceId from the original request — not the internal withdrawal id — so you can correlate the callback with the withdrawal you submitted. The internal id is provided separately as withdrawalId.

Withdrawal Confirmed Webhook

X-Webhook-Event: withdrawal.confirmed

{
  "type": "withdrawal",
  "withdrawalId": "cmdl8u2xq0001abcd1234efgh",
  "amount": "100.5",
  "currency": "USDT",
  "status": "confirmed",
  "txHash": "3f7a9c...",
  "referenceId": "order_123",
  "address": "TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
  "identifier": "order_123",
  "feeCharges": {
    "amount": "0",
    "percentage": "0.001",
    "type": "NONE",
    "networkFee": "1.2"
  },
  "receivableAmount": "100.5",
  "confirmedAt": "2026-09-03T10:15:22+08:00",
  "eventId": "cmdl8u2xq0002abcd1234efgh"
}

feeCharges is always present — amount is the commission margin above the network fee ("0" with type: "NONE" when your commission does not exceed it); networkFee is the per-chain fee quoted at submission. confirmedAt is in GMT+8 (+08:00).

Withdrawal Failed Webhook

X-Webhook-Event: withdrawal.failed

{
  "type": "withdrawal",
  "withdrawalId": "cmdl8u2xq0001abcd1234efgh",
  "amount": "100.5",
  "currency": "USDT",
  "status": "failed",
  "reason": "Withdrawal failed",
  "txHash": "3f7a9c...",
  "referenceId": "order_123",
  "address": "TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5",
  "identifier": "order_123",
  "eventId": "cmdl8u2xq0003abcd1234efgh"
}

On failure the reserved amount (including fees) is returned to your balance and your referenceId is released, so you can safely retry with the same referenceId.

Error Handling

Common Errors

Status CodeErrorDescription
400Invalid request dataValidation failed — details lists the offending fields
400Withdrawal amount must be greater than zeroAmount is zero or negative
400Withdrawals of {currency} on {network} are not supportedThe (currency, network) pair is unknown or disabled for withdrawals
400Amount below minimum withdrawalAmount below the pair's minimum — response includes minWithdrawal, currency, network
400No {currency} balance on {network} for this accountYou hold no balance on that chain
400Insufficient balanceAvailable balance on that chain doesn't cover amount + fees
401Authentication requiredMissing or invalid API key
403Insufficient permissions to initiate a withdrawalAPI key lacks write/payout capability
403Merchant account is not activeYour merchant account is suspended or inactive
409Duplicate referenceIdThis referenceId is already used by your merchant account for a different request (an identical retry returns the original withdrawal with 200)
503Withdrawal temporarily unavailableWithdrawals are temporarily unavailable — retry shortly or contact support

Example Error Responses

Insufficient balance (per-chain):

{
  "error": "Insufficient balance",
  "message": "Insufficient USDT balance on TRX. Deposits and withdrawals are per-chain: funds on other networks cannot be used.",
  "available": "50.25",
  "requested": "100.50",
  "currency": "USDT",
  "network": "TRX",
  "feeInfo": {
    "grossAmount": "100.5",
    "netAmount": "98.5",
    "totalFees": "2",
    "feeBreakdown": {
      "baseFee": "1",
      "markupRate": "0",
      "markupAmount": "1",
      "networkFee": "1",
      "totalFee": "2"
    }
  }
}

Duplicate referenceId:

{
  "error": "Duplicate referenceId",
  "message": "A withdrawal with this referenceId already exists for your merchant account with different parameters. Use a new referenceId to submit a new withdrawal.",
  "withdrawalId": "wth_123"
}

A payout that cannot be sent after the request was accepted is reported with a withdrawal.failed webhook (status FAILED); the reserved amount including fees is returned to your balance and the referenceId is released.

See Error Handling for the full guide.

Best Practices

1. Address Validation

Always validate addresses before creating withdrawals:

function validateAddress(network, address) {
  const patterns = {
    'TRX': /^T[a-zA-Z0-9]{33}$/,
    'ETH': /^0x[a-fA-F0-9]{40}$/,
    'BNB': /^0x[a-fA-F0-9]{40}$/
  };

  const pattern = patterns[network];
  if (!pattern) {
    throw new Error(`Unsupported network: ${network}`);
  }

  if (!pattern.test(address)) {
    throw new Error(`Invalid ${network} address: ${address}`);
  }

  return true;
}

A well-formed address is not necessarily a safe one to pay. Before a payout, screen the destination:

  • TRON: POST /api/compliance-checks with { "address": "T…" } returns an instant verdict. Don't send when verdict.blocked is true — aml_risk (sanctions or fraud labels), usdt_frozen (USDT sent there is frozen by Tether and cannot be recovered) or smart_contract. 1.00 per verdict; see TRON Address Compliance Checks.
  • Any live network: POST /api/aml-checks returns an Elliptic risk score (0–10), risk level and sanctions exposure for TRX, BNB and POL addresses. 1.50 per result, free when no result is delivered; see AML Address Checks.

2. Balance Checking

Always check the per-chain balance (including fees) before withdrawal:

async function safeWithdrawal(currency, network, amount, destination, referenceId) {
  // 1. Validate address
  validateAddress(network, destination);

  // 2. Check balance on the target chain
  await validateWithdrawalBalance(currency, network, amount);

  // 3. Create withdrawal
  const withdrawal = await createWithdrawal(currency, network, amount, destination, referenceId);

  // 4. Log withdrawal
  console.log(`Withdrawal created: ${withdrawal.withdrawalId}`);
  console.log(`Amount: ${withdrawal.amount} ${currency} on ${network}`);
  console.log(`Fees (on top): ${withdrawal.feeInfo.totalFees} ${currency}`);

  return withdrawal;
}

3. Idempotency with referenceId

Pass your own referenceId in the request body to prevent duplicate payouts. The referenceId is unique per merchant:

  • Retrying the same request with the same referenceId (same amount, currency, network, destination) returns 200 with the original withdrawal and "replayed": true — safe after a timeout. A second payout is never created.
  • Reusing a referenceId for a different request returns 409 Duplicate referenceId.
  • If a withdrawal fails, its referenceId is automatically released so you can retry with the same reference.
async function createWithdrawalIdempotent(currency, network, amount, destination, orderId) {
  const response = await fetch('https://api.dcepay.io/api/withdrawals', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.DCE_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      currency,
      network,
      amount,
      destination,
      referenceId: orderId  // your idempotency key, unique per merchant
    })
  });

  if (response.status === 409) {
    // This orderId was already used for a DIFFERENT withdrawal — do not
    // retry with a new referenceId unless you intend a second payout.
    console.log(`referenceId ${orderId} is taken by withdrawal ${(await response.json()).withdrawalId}`);
    return null;
  }

  // 200 for a new withdrawal and for a retry of the same one ("replayed": true).
  return response.json();
}

4. Webhook Handling

Handle withdrawal webhooks properly. Delivery is at-least-once, so make handlers idempotent (deduplicate on eventId), and acknowledge with { "ok": true }:

app.post('/webhooks', async (req, res) => {
  const event = req.headers['x-webhook-event'];
  const payload = req.body;

  // Deduplicate — the same eventId may be delivered more than once
  if (await alreadyProcessed(payload.eventId)) {
    return res.status(200).json({ ok: true });
  }

  switch (event) {
    case 'withdrawal.confirmed':
      await handleWithdrawalConfirmed(payload);
      break;
    case 'withdrawal.failed':
      await handleWithdrawalFailed(payload);
      break;
    default:
      console.log(`Unhandled webhook event: ${event}`);
  }

  res.status(200).json({ ok: true });
});

async function handleWithdrawalConfirmed(payload) {
  const { withdrawalId, referenceId, amount, address, txHash } = payload;

  // referenceId is YOUR reference from the original request
  await updateWithdrawalStatus(referenceId, 'confirmed', txHash);
}

async function handleWithdrawalFailed(payload) {
  const { referenceId, reason } = payload;

  await updateWithdrawalStatus(referenceId, 'failed', null, reason);
  // Safe to retry the payout with the same referenceId
}

Integration Example

Here's a complete withdrawal service implementation:

class WithdrawalService {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.dcepay.io/api';
  }

  async createWithdrawal(currency, network, amount, destination, referenceId, description) {
    const response = await fetch(`${this.baseUrl}/withdrawals`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        currency,
        network,
        amount,       // decimal string
        destination,
        referenceId,  // idempotency key, unique per merchant
        description
      })
    });

    const body = await response.json();

    if (response.status === 409) {
      throw new Error(`Duplicate referenceId: ${referenceId}`);
    }
    if (!response.ok) {
      throw new Error(`Withdrawal creation failed: ${body.message || body.error}`);
    }

    return body;
  }

  async getWithdrawals(filters = {}) {
    const params = new URLSearchParams(filters); // status, page, limit
    const response = await fetch(`${this.baseUrl}/withdrawals?${params}`, {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`
      }
    });

    return response.json();
  }

  async getBalances() {
    const response = await fetch(`${this.baseUrl}/balance`, {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`
      }
    });

    return response.json();
  }
}

// Usage
const withdrawalService = new WithdrawalService(process.env.DCE_API_KEY);

try {
  const withdrawal = await withdrawalService.createWithdrawal(
    'USDT', 'TRX', '100.50',
    'TXYZa1b2c3d4e5f6g7h8i9j0k1l2m3n4o5',
    'order_123'
  );

  console.log('Withdrawal created:', withdrawal.withdrawalId);
  console.log('Fees charged on top:', withdrawal.feeInfo.totalFees);
} catch (error) {
  console.error('Withdrawal failed:', error.message);
}

Next Steps

Now that you understand withdrawals, explore:

  1. Balance Management - Monitor your per-chain account balances
  2. Transactions - Track withdrawal lifecycle and statuses
  3. Webhooks - Handle withdrawal notifications
  4. TRON Address Compliance Checks - Screen a TRON payout destination before you send

Did this page help you?