Withdrawal fees
An external crypto withdrawal pays a flat fee, denominated in the asset being withdrawn. It is not a percentage: withdrawing 10 USDT and withdrawing 284,000 USDT cost the same.
Internal withdrawals (Skipo account to Skipo account) and bank payouts pay no fee.
:::info This is not the network fee The fee you see in the API is the one Skipo charges. The on-chain gas cost is absorbed by Skipo and is never billed to you or shown in your movements. :::
feeMode
POST /v2/withdrawals takes an optional feeMode field that decides how amount is read:
feeMode | amount is… | Debited | Arrives at destination |
|---|---|---|---|
deduct (default) | the total to debit | amount | amount − fee |
add | the amount that must arrive | amount + fee | amount |
deduct is what Binance, Kraken, Coinbase and OKX do. It is the one you want for sweeping a
balance: send the balance and you are done.
add is for when the arriving figure has to be exact — settling an invoice, a payroll line,
topping an address up to a specific number.
Both modes are equally supported and the response is identical in shape: it always carries all three numbers, so you never have to re-derive anything.
{
"amount": "999.5", // what is sent to the destination
"fee": "0.5", // Skipo's fee
"total": "1000" // what leaves the balance
}
total = amount + fee holds in both modes. The only thing that changes is which of the two you
supplied.
Example
A balance of 1,000 USDT on BNB Smart Chain, fee 0.5 USDT.
POST /v2/withdrawals
{ "assetSymbol": "USDT", "amount": "1000", "contactId": "..." }
With the deduct default, 1,000 USDT is debited and 999.5 arrives. For exactly 1,000 to arrive,
send "amount": "1000" with "feeMode": "add" — 1,000.5 will be debited.
:::caution Coming from v1, or integrated before August 2026
This endpoint previously behaved like add. If your code already sends balance − fee, add
"feeMode": "add" and it keeps working exactly as before, with no change to your arithmetic.
:::
The fee depends on the NETWORK, and the body does not name one
POST /v2/withdrawals takes assetSymbol, amount and the contact. It does not take a
network. The chain — and therefore the fee — is whichever one the destination contact is
registered on.
This matters because the same asset can cost very different amounts per chain. USDT is the live
case: 0.5 USDT on BNB Smart Chain, 6 USDT on Tron. Same assetSymbol, twelve times the fee.
:::danger Do not use the asset-level withdrawalFee
In GET /v2/assets, the withdrawalFee hanging off the asset is the cheapest chain's. For
USDT that is 0.5, and sizing a Tron withdrawal with it understates the cost by 5.5 USDT.
The authoritative value is networks[].withdrawalFee.
:::
How to read the right fee
// 1. Which chain is this destination on?
const contact = await get(`/v2/contacts/${contactId}`)
const networkId = contact.crypto.networkId // "BSC" | "TRON" | "ETH" | ...
// 2. The fee and minimum for THAT chain.
const asset = await get('/v2/assets?assetSymbol=USDT')
const network = asset.networks.find((n) => n.networkId === networkId)
network.withdrawalFee // what Skipo charges on this chain
network.minimumWithdrawal // the effective minimum on this chain
networkId means the same thing on /v2/assets, /v2/contacts and on movements, so they join
without parsing prose.
Minimums
networks[].minimumWithdrawal is the effective minimum for that chain: it already resolves
to the larger of the platform floor and the chain's own floor. Validate against it rather than
the asset-level minimumWithdrawal, which reports only the platform half and can sit below what
the chain will accept.
The minimum applies to what is sent. Under the deduct default that is amount − fee, so
the smallest amount a chain accepts is minimumWithdrawal + withdrawalFee.
On some assets the fee sits near or above the minimum — DOGE charges 5 and permits a minimum of 5 — so compute it rather than assuming.
Related errors
| Situation | Response |
|---|---|
Balance does not cover total | 422 unprocessable — Insufficient available balance for this withdrawal. |
With deduct, amount does not cover the fee | 422 unprocessable — nothing would reach the destination. The account may be perfectly well funded: what is missing is amount, not funds. |
| Amount falls below the chain's minimum | 422 unprocessable |
A rejection moves nothing: the balance is checked before any funds are held.
And if an accepted withdrawal later fails, the full total — amount and fee — is returned.
Skipo does not keep the fee on a failed send.
Where the fee shows up afterwards
GET /v2/withdrawals and GET /v2/withdrawals/{id} return amount, fee and total on every
withdrawal, the same as the creation response. The withdrawal.status.updated webhook publishes
the same three fields.
A withdrawal is one row, fee included: the fee is never emitted as a separate movement, so
summing total over the list gives the account's real outflow with no filtering.
See also: Withdrawals · Assets · Contacts.