Withdrawals
Return funds from a user's manager to their external wallet — wallet-level and per-order.
Funds in a user's manager always come back to that user's own external wallet — the manager's policy allows nothing else. There are two ways to move them, both following the same intent → sign → confirm pattern as order creation.
Wallet-level withdrawals
These pull a chosen token out of the manager regardless of any order. Use them to power a "withdraw available balance" action.
Check what's available first:
const { data } = await callTitanDca('/me/balance?hideZero=true', { sub });
// each balance row: totalBalance, lockedForFutureTxns, withdrawalPending, availableToWithdrawavailableToWithdraw is max(total − locked − withdrawalPending, 0) — the source of truth for a "withdraw max" button. Active orders lock their unspent input, so it won't be available until the order ends: for a DCA order or a running Slice Order that's totalAmount − amountSpent, and for a trigger order it's the full unspent amount while it's armed. Output mints are not locked, with one exception: tokens bought by a conditional entry stay locked from the moment its swap is sent until they become the bracket's amount.
lockedBreakdown on each row attributes the lock to specific orders, with orderType being any of dca, stop_loss, take_profit, oco, or slice — enough to tell the user which order is holding their balance.
Build the withdrawal tx, omitting amount to withdraw the max:
const intent = await callTitanDca('/withdraw/transaction', {
method: 'POST',
sub,
body: {
userPubkey, // fee payer & destination
tokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
amount: '400000', // optional; omit for max
},
});
// user signs intent.data.transaction, then:
await callTitanDca('/withdraw/confirm', {
method: 'POST', sub,
body: { signedTransaction },
});Titan re-runs the lock-aware check at confirm time using the (mint, amount) from the unsigned transaction it returned — so if that transaction goes stale during its 5-minute window (e.g. another withdrawal landed in the meantime), it's rejected before broadcast rather than overdrawing.
HTTP
error.code
When
400
INSUFFICIENT_AVAILABLE
The whole balance is locked, or the wallet is empty.
400
FUNDS_LOCKED
requested > available — some balance is locked by orders or a pending withdrawal. details has the breakdown.
400
INSUFFICIENT_BALANCE
requested > chainBalance — the wallet genuinely doesn't hold that much.
409
ONBOARDING_INCOMPLETE
Manager not fully provisioned. Re-call onboard, then retry.
Order-level withdrawals
These return the funds tied to a single terminal order (completed / cancelled / failed / expired) back to the external wallet.
A completed trigger order or Slice Order is also a no-op here: its fills consumed the whole deposit and the output was delivered at execution time — or belongs to a bracket order — so it returns 400 NOTHING_TO_WITHDRAW. For trigger orders, order-level withdrawal really only matters after a cancel. Slice Orders cannot be cancelled, so it matters only in the brief window before a failed one is auto-returned.
To exit a bracket early, cancel the bracket order with withdraw: true — its withdrawal returns the bought tokens. See Conditional Entry & Bracket.
POST /orders/{orderId}/withdraw is safe to retry — each call rebuilds a fresh tx (so the user can re-prompt their wallet) and resets the inactivity timer on the automatic recovery process. withdrawalAmounts enumerates the per-mint amounts the transaction will move, one entry per non-zero mint, which is what you show the user before they sign.
HTTP
error.code
When
400
NOTHING_TO_WITHDRAW
No order-owned funds remain (the user likely drained them via a wallet-level withdrawal).
400
INVALID_STATE
Order isn't completed / cancelled / failed / expired.
400
ALREADY_WITHDRAWN
A prior withdrawal already completed — including the automatic return on a failed order or an expired trigger order.
409
EXECUTION_IN_FLIGHT
A swap attempt is still being reconciled. Self-resolving — retry shortly.
503
RPC_UNAVAILABLE
Couldn't fetch on-chain balance. Retry shortly.
Releasing a stuck withdrawal lock
If the user dismisses the wallet prompt from POST /orders/{orderId}/withdraw, the order sits in withdrawalStatus: pending. Call POST /orders/{orderId}/withdraw/abandon to make the "Withdraw" button usable again immediately — it's idempotent. Without it the lock clears on its own, but not quickly — expect up to 8 minutes. That's long enough for a user to notice, which is why abandon exists.
HTTP
error.code
When
400
ALREADY_WITHDRAWN
The withdrawal already completed — nothing to abandon.
404
NOT_FOUND
Order missing or not yours.
409
WITHDRAWAL_IN_FLIGHT
Lost the race to /withdraw/confirm. Retry briefly.
Related pages
Managing Orders — cancel-with-withdraw builds an order-level withdrawal in one call
Lifecycle & Polling —
withdrawalStatustransitions and automatic input returnError Codes — the full withdrawal error catalog
Last updated

