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 balanceerror means youractiveBalancefor that token (andwalletId, 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 sendwalletId. OptionalisAutoConvertcan cover a shortfall from other same-peg tokens.per_wallet— ledger per deposit wallet.walletIdis required on create (must match the token’s network).isAutoConvertis 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).
| Concept | Field(s) | Meaning |
|---|---|---|
| Rate | withdrawProfitFeePercentage | Percent of amount (the amount you request in the API), e.g. 0.5 = 0.5% |
| Fee amount | withdrawProfitFee | Token units deducted or included per fee mode: withdrawProfitFee = amount × (withdrawProfitFeePercentage ÷ 100) (rounded) |
| Gas | withdrawGasFee | Estimated network cost in the same token — not part of the profit fee |
| Ledger | Transaction history | Successful 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. totalDeductedfrom 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
| Method | Path | Description |
|---|---|---|
POST | /api/v1/withdraws/estimate | Quote fees and amounts (no balance reserved) |
POST | /api/v1/withdraws | Create a withdrawal |
GET | /api/v1/withdraws | List withdrawals |
GET | /api/v1/withdraws/:id | Get 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.
| Field | Type | Description |
|---|---|---|
id | string | Withdrawal UUID |
amount | number | Requested withdrawal amount (token units) |
withdrawGasFee | number | Network gas component (token units) |
withdrawProfitFee | number | Platform profit fee in token units (see Withdraw profit fee) |
withdrawProfitFeePercentage | number | Snapshot of the app’s profit fee percent at create time (e.g. 0.5 = 0.5%) |
withdrawTransactionFee | string | exclusive or inclusive (see Fee modes) |
netAmount | number | Amount sent on-chain to recipientAddress |
totalDeducted | number | Off-chain amount reserved from activeBalance |
amountInDecimals | string | On-chain send amount (netAmount) in smallest token units (integer string) |
networkTokenCode | string | Allowed token code |
network | string | Network code (e.g. polygon-pos, ethereum) |
recipientAddress | string | Destination address |
reference | string | null | Your reference, if provided |
status | string | pending, queued, success, failed, or canceled |
failedReason | string | null | Present when status is failed |
withdrawWalletId | string | Hot wallet UUID used to broadcast the transfer |
walletId | string | null | Ledger wallet UUID (per_wallet apps only) |
isAutoConvert | boolean | Present when the request set isAutoConvert: true |
conversions | array | Same-peg convert steps (see Auto-convert). Omitted or empty when none ran |
signRequestId | string | null | Internal MPC sign request (when set) |
signAuthorizationRequestId | string | null | Internal EIP-7702 authorization request (EVM) |
signTypedDataRequestId | string | null | Internal typed-data sign request (EVM) |
Status lifecycle
| Status | Meaning |
|---|---|
pending | Created; balance withheld; signing / broadcast in progress |
queued | Created while another withdraw is in flight; balance already withheld; starts when the active withdraw finishes |
success | On-chain transfer completed |
failed | Processing failed; withheld balance is released back to activeBalance |
canceled | Canceled (dashboard flows) |
Subscribe to webhooks (withdraw.created, withdraw.success, withdraw.failed) for async updates.
Fee modes
Configured per app as withdrawTransactionFee:
| Mode | netAmount | totalDeducted from off-chain balance |
|---|---|---|
exclusive | Full amount | amount + withdrawGasFee + withdrawProfitFee |
inclusive | amount - withdrawGasFee - withdrawProfitFee | amount (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):
- Cover
totalDeducted(requestedamountplus exclusive fees), not justamount. - If destination
activeBalancealready covers the withdraw, skip convert (conversionsomitted or empty). - Otherwise convert from other active allowed tokens with the same
peggedTo(e.g. USDT and USDC can cover each other; XSGD cannot cover USDC). - Source order: same network as the destination token, then largest
activeBalance, thentokenCode. - Convert only the shortfall, 1:1 (
amountequalstoAmount). - If destination plus those sources is still short, fail with
insufficient active balanceand write nothing.
per_wallet apps sending isAutoConvert: true get 400 auto-convert is only allowed for per_app apps.
Each item in conversions:
| Field | Type | Description |
|---|---|---|
from | string | Source allowed token code |
to | string | Destination allowed token code (the withdraw tokenCode) |
amount | number | Amount converted from from |
toAmount | number | Amount 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
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount to withdraw (> 0) |
tokenCode | string | Yes | Allowed token code (see Request field: tokenCode) |
recipientAddress | string | Yes | EVM: 0x + 40 hex chars. Tron: T + 33 base58 chars |
isAutoConvert | boolean | No | Default 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.
| Code | Typical error message |
|---|---|
| 400 | amount must be greater than zero |
| 400 | recipient address is required |
| 400 | invalid recipient address |
| 400 | token not found |
| 400 | token is not active |
| 400 | network type not supported for withdraw |
| 400 | withdraw wallet not configured for network |
| 400 | withdraw wallet insufficient token balance |
| 400 | withdraw fee exceeds requested amount for inclusive fee mode |
| 400 | auto-convert is only allowed for per_app apps |
| 400 | insufficient active balance: you have X, need Y |
| 403 | app is frozen |
Create Withdraw
POST /api/v1/withdraws
Same body fields as estimate, plus:
| Field | Type | Required | Description |
|---|---|---|---|
reference | string | No | Stored on the withdraw; empty strings are ignored |
walletId | string | Conditional | Required for per_wallet apps. Must not be sent for per_app |
isAutoConvert | boolean | No | Default 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).
| Code | Typical error message |
|---|---|
| 400 | Validation errors (same as estimate) |
| 400 | walletId is required |
| 400 | wallet not found |
| 400 | wallet network does not match token network |
| 400 | walletId is not allowed for per_app balance type |
| 400 | auto-convert is only allowed for per_app apps |
| 400 | insufficient active balance: you have X, need Y |
| 403 | app is frozen |
| 503 | mpc signing service is unavailable |
List Withdraws
GET /api/v1/withdraws
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (minimum 1) |
limit | int | 20 | Page size (minimum 1) |
status | string | — | Exact match: pending, queued, success, failed, canceled |
networkTokenCode | string | — | Filter by allowed token code |
network | string | — | Filter by network code (e.g. ethereum) |
walletId | string | — | Filter by ledger wallet UUID (per_wallet) |
startDate | string | — | Inclusive lower bound on create time. RFC3339 or YYYY-MM-DD |
endDate | string | — | Inclusive upper bound on create time. RFC3339 or YYYY-MM-DD (YYYY-MM-DD = end of that UTC day) |
q | string | — | 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"
}
}| Code | Typical error message |
|---|---|
| 404 | withdraw not found |
Only withdrawals belonging to your app (via X-App-ID) are returned.