curl -X POST https://api.liddie.io/api/v1/withdrawals \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"currency":"LTC","amount":"0.5","address":"L..."}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/withdrawals', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currency: 'LTC',
amount: '0.5',
address: 'L...',
// ...plus one fresh MFA factor, e.g. emailCode / totpCode / passkeyResponse + challengeKey
}),
})
const body = await res.json()
{"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Verification required: provide email code, OTP, or passkey"}}
Withdrawals
Request a crypto withdrawal
Withdraw your Liddie balance to a whitelisted crypto address. You pay only the network fee, and fresh MFA verification is required on every call.
POST
/
api
/
v1
/
withdrawals
curl -X POST https://api.liddie.io/api/v1/withdrawals \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"currency":"LTC","amount":"0.5","address":"L..."}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/withdrawals', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currency: 'LTC',
amount: '0.5',
address: 'L...',
// ...plus one fresh MFA factor, e.g. emailCode / totpCode / passkeyResponse + challengeKey
}),
})
const body = await res.json()
{"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Verification required: provide email code, OTP, or passkey"}}
Moves available balance out to an address on your withdrawal whitelist. Egress is fee-free: only the network cost is deducted — there is no platform markup.
The example request omits the MFA fields, so the server rejects it with
Withdrawals move real funds and every request needs a fresh MFA factor (passkey, TOTP, or emailed code). Without one, the call fails with
VERIFICATION_FAILED — status 400 when the factor is missing, 401 when it is wrong or already used, 403 when TOTP is not enrolled, 429 after too many attempts.Cookie-authenticated writes also need an
Origin header. Any non-GET request that carries the liddie_access session cookie is checked against the allowed dashboard origins; a bare cURL that sends only the cookie is rejected with 403 {"ok":false,"error":{"code":"CSRF_ORIGIN_MISMATCH","message":"Forbidden"}} before the handler runs. The snippet below is shown for shape — from a browser the dashboard sends the origin for you; from a script, prefer an API key where the endpoint accepts one.curl -X POST https://api.liddie.io/api/v1/withdrawals \
-H "Cookie: <dashboard session cookies>" \
-H "Content-Type: application/json" \
-d '{"currency":"LTC","amount":"0.5","address":"L..."}'
// Dashboard-session auth: send the session JWT as a Bearer token
const res = await fetch('https://api.liddie.io/api/v1/withdrawals', {
method: 'POST',
headers: {
Authorization: `Bearer ${dashboardJwt}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currency: 'LTC',
amount: '0.5',
address: 'L...',
// ...plus one fresh MFA factor, e.g. emailCode / totpCode / passkeyResponse + challengeKey
}),
})
const body = await res.json()
{"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Verification required: provide email code, OTP, or passkey"}}
VERIFICATION_FAILED.
Authorization
Dashboard-only (JWT session). Roles:merchant_admin / merchant_member / super_admin, with team permission withdrawals:manage. The merchant account must be active. The body must additionally carry a fresh MFA factor (passkey / TOTP / emailed code); request an email code via Send withdrawal email code. Rate limit: 10/min.
Calling with an API key fails with 401 {"error":"Invalid or expired token"} — the key is not a JWT.
Parameters
string
required
Ticker of the balance to withdraw from, e.g.
LTC, USDT_TRC20.string
required
Decimal string. Must be ≤ your available ledger balance (the balance recorded for your account on the platform); the amount is atomically reserved.
string
required
Destination address. Must already be on your withdrawal whitelist for that currency.
string
Destination tag / memo for chains that use one (e.g. XRP). Part of the whitelist identity
(currency, address, destinationTag).MFA fields (dashboard sessions)
MFA fields (dashboard sessions)
string
The single-use emailed code.
string
A 6-digit code from your authenticator app — an alternative to
emailCode. Backup codes are not accepted here: the field is validated against ^[0-9]{6}$ and backup codes are 16 hexadecimal characters. They work only at login (POST /2fa/validate). If you have lost your authenticator, request an emailed code instead.object
WebAuthn assertion, used together with
challengeKey.string
Accompanies
passkeyResponse.Rules enforced server-side
Rules enforced server-side
addressmust be on your whitelist for that currency.amount(decimal string) must be ≤ your available ledger balance; the amount is atomically reserved.- The network fee is deducted from the amount sent (
netToDestinationin the response); no platform fee. - Without MFA:
400 VERIFICATION_FAILED. With a wrong or reused factor the same code arrives as401,403or429. - If your account has any whitelisted IP, the request must come from one of them, or it is refused with
403 IP_NOT_WHITELISTEDbefore the MFA factor is checked (so the code is not spent). An empty IP list imposes no restriction.
Response
200 OK with the accepted withdrawal inside the standard envelope. Every money value is an exact decimal string.
200 OK
{
"ok": true,
"data": {
"id": "665f1a2b3c4d5e6f70819200",
"currency": "USDT_TRC20",
"amount": "544.5",
"networkFee": "2.215218",
"netToDestination": "542.284782",
"destination": "TXSf6BhvjbSVqjwmcJpBDjRm81uvvvzvZV",
"status": "queued"
}
}
string
The withdrawal id. Use it to find the row again in List withdrawals.
string
Echoed back from the request.
string
The gross amount debited from your balance, as an exact decimal string.
string
The network fee withheld. Liddie adds no markup on egress.
string
What actually arrives at the destination:
amount minus networkFee.string
The destination address the withdrawal was accepted for.
string
Present only when the request carried one (XRP).
string
queued on acceptance. Processing is asynchronous — the row advances to completed with a txHash once broadcast and verified on-chain.The withdrawal is handled in the background after it is accepted. Track the row via List withdrawals — its status advances to completed with a
txHash once broadcast and verified.Errors
| Status | Body | Why |
|---|---|---|
| 400 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Verification required: provide email code, OTP, or passkey"}} | No (or invalid) MFA factor in the body. |
| 401 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Invalid email code"}} | Wrong, already-used or invalid email code, OTP or passkey assertion. Same code, different status — do not treat this 401 as an expired session. |
| 403 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"TOTP is not enabled on this account"}} | A totpCode was sent by a user with no authenticator enrolled. |
| 429 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Too many incorrect codes. Request a new one."}} | Five wrong email codes — the challenge is voided; request a fresh code. |
| 429 | {"ok":false,"error":{"code":"VERIFICATION_FAILED","message":"Too many failed attempts. Try again later."}} | TOTP lockout after repeated wrong codes. |
| 401 | {"error":"Invalid or expired token"} | Called with an API key (or an expired session) — this endpoint is dashboard-JWT only. |
| 403 | {"ok":false,"error":{"code":"IP_NOT_WHITELISTED","message":"This request came from 198.51.100.9, which is not on your withdrawal IP whitelist. Add that IP to your whitelisted IPs, then try again."}} | Your IP whitelist is non-empty and this address is not on it. Checked before MFA, so your emailed code is NOT consumed — whitelist the address and retry with the same one. An empty IP list never produces this. |
| 400 | {"ok":false,"error":{"code":"NOT_WHITELISTED","message":"Destination is not a whitelisted withdrawal address for this currency"}} | The (currency, address, tag) triple is not on your whitelist. This is the most common rejection after MFA. |
| 400 | {"ok":false,"error":{"code":"INSUFFICIENT_FUNDS","message":"Insufficient available balance"}} | The gross amount exceeds your available balance for that currency. |
| 400 | {"ok":false,"error":{"code":"INVALID_AMOUNT","message":"Amount must be a positive decimal"}} | amount is not a positive decimal. |
| 400 | {"ok":false,"error":{"code":"UNSUPPORTED_CURRENCY","message":"Unsupported currency"}} | The ticker is not in the registry. |
| 400 | {"ok":false,"error":{"code":"NO_DESTINATION","message":"Destination address is required"}} | address was empty. |
| 400 | {"ok":false,"error":{"code":"NO_SETTLEMENT_WALLET","message":"No settlement wallet for this currency"}} | You have no settlement wallet configured for that currency. |
| 400 | {"ok":false,"error":{"code":"INVALID_DESTINATION","message":"Destination cannot be the settlement wallet"}} | You tried to withdraw to your own settlement address. |
| 400 | {"ok":false,"error":{"code":"TAG_NOT_SUPPORTED","message":"Destination tags are not supported for this currency"}} | A destinationTag was sent for a non-XRP currency. |
| 400 | {"ok":false,"error":{"code":"INVALID_DESTINATION_TAG","message":"Invalid destination tag (digits only, 0 to 4294967295, no leading zeros)"}} | The tag is not a canonical uint32. |
| 400 | {"ok":false,"error":{"code":"FEE_ESTIMATE_FAILED","message":"Could not estimate the network fee right now. Try again shortly."}} | Transient — the fee estimator could not price the transfer. Retry with a backoff. |
| 400 | {"ok":false,"error":{"code":"AMOUNT_BELOW_FEE","message":"Amount must exceed the network fee (~<fee> <currency>)"}} | After deducting the estimated network fee the net to the destination is ≤ 0. Common on high-fee chains for small withdrawals. Fires after the emailed code is spent. |
| 400 | {"ok":false,"error":{"code":"MERCHANT_SUSPENDED","message":"Merchant account is suspended"}} | Defence in depth; the route guard normally answers 403 first. |
| 400 | {"ok":false,"error":{"code":"CREATE_FAILED","message":"Could not create the withdrawal"}} | The reserve or the row insert failed. Nothing was debited. |
These fire AFTER the MFA factor is verified, so an emailed code is already spent by the time you see one. Request a fresh code before retrying — do not resend the same body.
See also
- Add wallet address: whitelist the destination before it can receive a withdrawal.
- Send withdrawal email code: get the single-use email code this endpoint accepts as an MFA factor.
- List withdrawals: follow the withdrawal’s status until it completes with a
txHash.