Custodian
Withdraw

Withdraw

Send funds from your off-chain balance (activeBalance) to an external address. Estimate fees first, then create the withdrawal. Supported network types: EVM and Tron. All endpoints require authentication.

Balance type: Withdrawals spend off-chain balance. The insufficient active balance error means your activeBalance for that token (and walletId, if applicable) is too low. Xellar then sends tokens on-chain from a configured withdraw (hot) wallet.

  • per_app — one ledger per app + token. Do not send walletId. Optional isAutoConvert can cover a shortfall from other same-peg tokens.
  • per_wallet — ledger per deposit wallet. walletId is required on create (must match the token’s network). isAutoConvert is not allowed.

Withdraw profit fee (platform fee)

Your app has a configured withdrawProfitFeePercentage (set in the Xellar Panel / admin — not changeable via the signed API). It is a platform fee on the withdrawal amount, separate from network gas (withdrawGasFee).

ConceptField(s)Meaning
RatewithdrawProfitFeePercentagePercent of amount (the amount you request in the API), e.g. 0.5 = 0.5%
Fee amountwithdrawProfitFeeToken units deducted or included per fee mode: withdrawProfitFee = amount × (withdrawProfitFeePercentage ÷ 100) (rounded)
GaswithdrawGasFeeEstimated network cost in the same token — not part of the profit fee
LedgerTransaction historySuccessful withdraws can produce rows with transactionType withdraw_profit_fee (debit from off-chain balance)

Who pays what

  • Profit fee — Xellar’s configured take on the withdrawal amount (your product’s commercial terms).
  • Gas fee — Covers broadcasting the on-chain transfer from the hot wallet.

Both fees interact with withdrawTransactionFee (exclusive vs inclusive) — see Fee modes. Estimate and Create always return the same fee fields so you can show a full quote before committing.

Example (exclusive, amount 100, gas 1, profit 0.5% → profit fee 0.5):

  • Recipient receives netAmount = 100 on-chain.
  • totalDeducted from your off-chain balance = 101.5 (100 + 1 + 0.5).

Example (inclusive, same numbers):

  • Recipient receives netAmount = 98.5 (100 − 1 − 0.5).
  • totalDeducted = 100.

Endpoints

MethodPathDescription
POST/api/v1/withdraws/estimateQuote fees and amounts (no balance reserved)
POST/api/v1/withdrawsCreate a withdrawal
GET/api/v1/withdrawsList withdrawals
GET/api/v1/withdraws/:idGet one withdrawal

Request field: tokenCode

Estimate and Create require tokenCode: an allowed token code from GET /api/v1/tokens (e.g. usdt-ethereum, usdc-polygon-pos). This is not the network code (ethereum, polygon-pos, …).

Responses still expose both networkTokenCode (same token code) and network (chain code). List query filters networkTokenCode and network are unchanged. The Xellar Panel dashboard may still label this field network in its own API — the signed client API uses tokenCode.

Withdraw object

Returned by Create, Get, and each item in List.

FieldTypeDescription
idstringWithdrawal UUID
amountnumberRequested withdrawal amount (token units)
withdrawGasFeenumberNetwork gas component (token units)
withdrawProfitFeenumberPlatform profit fee in token units (see Withdraw profit fee)
withdrawProfitFeePercentagenumberSnapshot of the app’s profit fee percent at create time (e.g. 0.5 = 0.5%)
withdrawTransactionFeestringexclusive or inclusive (see Fee modes)
netAmountnumberAmount sent on-chain to recipientAddress
totalDeductednumberOff-chain amount reserved from activeBalance
amountInDecimalsstringOn-chain send amount (netAmount) in smallest token units (integer string)
networkTokenCodestringAllowed token code
networkstringNetwork code (e.g. polygon-pos, ethereum)
recipientAddressstringDestination address
referencestring | nullYour reference, if provided
statusstringpending, queued, success, failed, or canceled
failedReasonstring | nullPresent when status is failed
withdrawWalletIdstringHot wallet UUID used to broadcast the transfer
walletIdstring | nullLedger wallet UUID (per_wallet apps only)
isAutoConvertbooleanPresent when the request set isAutoConvert: true
conversionsarraySame-peg convert steps (see Auto-convert). Omitted or empty when none ran
signRequestIdstring | nullInternal MPC sign request (when set)
signAuthorizationRequestIdstring | nullInternal EIP-7702 authorization request (EVM)
signTypedDataRequestIdstring | nullInternal typed-data sign request (EVM)

Status lifecycle

StatusMeaning
pendingCreated; balance withheld; signing / broadcast in progress
queuedCreated while another withdraw is in flight; balance already withheld; starts when the active withdraw finishes
successOn-chain transfer completed
failedProcessing failed; withheld balance is released back to activeBalance
canceledCanceled (dashboard flows)

Subscribe to webhooks (withdraw.created, withdraw.success, withdraw.failed) for async updates.

Fee modes

Configured per app as withdrawTransactionFee:

ModenetAmounttotalDeducted from off-chain balance
exclusiveFull amountamount + withdrawGasFee + withdrawProfitFee
inclusiveamount - withdrawGasFee - withdrawProfitFeeamount (must leave a positive net)

Auto-convert (isAutoConvert)

Optional boolean on signed Estimate and Create. Default false. Dashboard (Xellar Panel) withdraw does not take this flag.

When false or omitted, the destination token’s activeBalance must already cover totalDeducted.

When true (per_app only):

  1. Cover totalDeducted (requested amount plus exclusive fees), not just amount.
  2. If destination activeBalance already covers the withdraw, skip convert (conversions omitted or empty).
  3. Otherwise convert from other active allowed tokens with the same peggedTo (e.g. USDT and USDC can cover each other; XSGD cannot cover USDC).
  4. Source order: same network as the destination token, then largest activeBalance, then tokenCode.
  5. Convert only the shortfall, 1:1 (amount equals toAmount).
  6. If destination plus those sources is still short, fail with insufficient active balance and write nothing.

per_wallet apps sending isAutoConvert: true get 400 auto-convert is only allowed for per_app apps.

Each item in conversions:

FieldTypeDescription
fromstringSource allowed token code
tostringDestination allowed token code (the withdraw tokenCode)
amountnumberAmount converted from from
toAmountnumberAmount credited to to (same as amount; 1:1)

Example: exclusive withdraw of 15 usdc-ethereum, dest ledger has 10 USDC, same-peg USDT has 10 → convert 5 USDT → USDC, then deduct 15 (+ fees) from USDC. Estimate returns the planned conversions; Create runs them in the same transaction as the withhold.


Estimate Withdraw

POST /api/v1/withdraws/estimate

Computes fees and deduction amounts. Does not reserve balance. Does not accept walletId (ledger wallet only matters on create for per_wallet apps).

Request Body

FieldTypeRequiredDescription
amountnumberYesAmount to withdraw (> 0)
tokenCodestringYesAllowed token code (see Request field: tokenCode)
recipientAddressstringYesEVM: 0x + 40 hex chars. Tron: T + 33 base58 chars
isAutoConvertbooleanNoDefault false. If true, quote same-peg cover for a dest-token shortfall (per_app only). See Auto-convert
{
  "amount": 100,
  "tokenCode": "usdt-ethereum",
  "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "isAutoConvert": true
}

Response 200

{
  "data": {
    "amount": 50,
    "withdrawGasFee": 0.5,
    "withdrawProfitFee": 0.25,
    "withdrawProfitFeePercentage": 0.5,
    "withdrawTransactionFee": "exclusive",
    "netAmount": 50,
    "totalDeducted": 50.75,
    "networkTokenCode": "usdt-ethereum",
    "network": "ethereum",
    "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "isAutoConvert": true,
    "conversions": [
      {
        "from": "usdt-polygon-pos",
        "to": "usdt-ethereum",
        "amount": 0.75,
        "toAmount": 0.75
      }
    ]
  }
}

isAutoConvert and conversions are omitted when you did not request auto-convert, or when dest activeBalance already covers totalDeducted.

CodeTypical error message
400amount must be greater than zero
400recipient address is required
400invalid recipient address
400token not found
400token is not active
400network type not supported for withdraw
400withdraw wallet not configured for network
400withdraw wallet insufficient token balance
400withdraw fee exceeds requested amount for inclusive fee mode
400auto-convert is only allowed for per_app apps
400insufficient active balance: you have X, need Y
403app is frozen

Create Withdraw

POST /api/v1/withdraws

Same body fields as estimate, plus:

FieldTypeRequiredDescription
referencestringNoStored on the withdraw; empty strings are ignored
walletIdstringConditionalRequired for per_wallet apps. Must not be sent for per_app
isAutoConvertbooleanNoDefault false. If true, convert same-peg holdings to cover a dest-token shortfall (per_app only). See Auto-convert
{
  "amount": 50,
  "tokenCode": "usdt-ethereum",
  "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "reference": "WD-2026-001",
  "walletId": "5326036c-50c3-4d63-8150-69cb253542e3"
}

For per_app apps, omit walletId. Optional isAutoConvert:

{
  "amount": 50,
  "tokenCode": "usdt-ethereum",
  "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "reference": "WD-2026-001",
  "isAutoConvert": true
}

Response 201

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "amount": 50,
    "withdrawGasFee": 0.5,
    "withdrawProfitFee": 0.25,
    "withdrawProfitFeePercentage": 0.5,
    "withdrawTransactionFee": "exclusive",
    "netAmount": 50,
    "totalDeducted": 50.75,
    "amountInDecimals": "50000000",
    "networkTokenCode": "usdt-ethereum",
    "network": "ethereum",
    "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "reference": "WD-2026-001",
    "status": "pending",
    "failedReason": null,
    "withdrawWalletId": "8f2e1b0a-9c8d-7e6f-5a4b-3c2d1e0f9a8b",
    "walletId": "5326036c-50c3-4d63-8150-69cb253542e3",
    "isAutoConvert": true,
    "conversions": [
      {
        "from": "usdt-polygon-pos",
        "to": "usdt-ethereum",
        "amount": 0.75,
        "toAmount": 0.75
      }
    ],
    "signAuthorizationRequestId": "mpc-req-uuid-example"
  }
}

If another withdraw is already in flight, status may be queued instead of pending. MPC signing fields appear as the pipeline progresses. isAutoConvert / conversions appear only when requested and steps ran (or were planned).

CodeTypical error message
400Validation errors (same as estimate)
400walletId is required
400wallet not found
400wallet network does not match token network
400walletId is not allowed for per_app balance type
400auto-convert is only allowed for per_app apps
400insufficient active balance: you have X, need Y
403app is frozen
503mpc signing service is unavailable

List Withdraws

GET /api/v1/withdraws

Query Parameters

ParamTypeDefaultDescription
pageint1Page number (minimum 1)
limitint20Page size (minimum 1)
statusstring—Exact match: pending, queued, success, failed, canceled
networkTokenCodestring—Filter by allowed token code
networkstring—Filter by network code (e.g. ethereum)
walletIdstring—Filter by ledger wallet UUID (per_wallet)
startDatestring—Inclusive lower bound on create time. RFC3339 or YYYY-MM-DD
endDatestring—Inclusive upper bound on create time. RFC3339 or YYYY-MM-DD (YYYY-MM-DD = end of that UTC day)
qstring—Search by withdraw id, transaction hash, recipient address, or reference

Invalid startDate / endDate → 400 with invalid startDate; use RFC3339 or YYYY-MM-DD (or endDate). If startDate is after endDate → 400 with startDate must not be after endDate.

Response 200

{
  "data": {
    "items": [
      {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "amount": 50,
        "withdrawGasFee": 0.5,
        "withdrawProfitFee": 0.25,
        "withdrawProfitFeePercentage": 0.5,
        "withdrawTransactionFee": "exclusive",
        "netAmount": 50,
        "totalDeducted": 50.75,
        "amountInDecimals": "50000000",
        "networkTokenCode": "usdt-ethereum",
        "network": "ethereum",
        "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
        "reference": "WD-2026-001",
        "status": "success",
        "failedReason": null,
        "withdrawWalletId": "8f2e1b0a-9c8d-7e6f-5a4b-3c2d1e0f9a8b",
        "walletId": "5326036c-50c3-4d63-8150-69cb253542e3"
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}

Results are ordered by created_at descending (newest first).


Get Withdraw

GET /api/v1/withdraws/:id

:id is the withdrawal UUID returned from Create.

Response 200

Same object shape as in Create (see Withdraw object).

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "amount": 50,
    "withdrawGasFee": 0.5,
    "withdrawProfitFee": 0.25,
    "withdrawProfitFeePercentage": 0.5,
    "withdrawTransactionFee": "exclusive",
    "netAmount": 50,
    "totalDeducted": 50.75,
    "amountInDecimals": "50000000",
    "networkTokenCode": "usdt-ethereum",
    "network": "ethereum",
    "recipientAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "reference": "WD-2026-001",
    "status": "success",
    "failedReason": null,
    "withdrawWalletId": "8f2e1b0a-9c8d-7e6f-5a4b-3c2d1e0f9a8b",
    "walletId": "5326036c-50c3-4d63-8150-69cb253542e3"
  }
}
CodeTypical error message
404withdraw not found

Only withdrawals belonging to your app (via X-App-ID) are returned.