Skip to main content
Portfolio wallet withdrawals are asynchronous. A withdrawal may involve unwinding yield positions, bridging across chains, and waiting for customer approval — all orchestrated automatically.

Withdrawal lifecycle

The typical flow is: preview → initiate → track → complete.
  1. Preview — call withdrawal-preview to see what’s available and get a sourcing plan.
  2. Initiate — call withdraw to create the withdrawal. The system reserves liquidity and begins sourcing.
  3. Track — poll the withdrawal or subscribe to webhooks. The withdrawal moves through statuses as positions unwind, funds bridge, and payouts execute.
  4. Complete — the withdrawal reaches completed when all payout legs are terminal.

Preview a withdrawal

Before initiating, preview what balance is available for a given destination and how liquidity will be sourced.
curl -X POST "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawal-preview" \
  -H "Authorization: Bearer $BRAID_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "destinationChain": "polygon",
    "destinationToken": "usdc",
    "withdrawalAmount": "65000"
  }'
{
  "walletId": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
  "computedAt": "2026-02-05T10:06:10.000Z",
  "withdrawalAmount": 65000,
  "destinationChain": "polygon",
  "destinationToken": "usdc",
  "balance": 82500,
  "pendingWithdrawals": 0,
  "availableBalance": 82500,
  "positions": [
    { "positionKey": "cash_usdc:cctp", "amount": 30000 },
    { "positionKey": "usdz", "amount": 35000 }
  ]
}
FieldDescription
balanceTotal value in bridge domain(s) that can satisfy this destination (not wallet-wide)
pendingWithdrawalsSum of active liquidity reservations in those bridge domain(s)
availableBalancebalance - pendingWithdrawals (unreserved liquidity)
positions[]Best-effort sourcing plan: cash first, then more-liquid positions, then less-liquid
Withdrawals are constrained by bridge domains — hard liquidity boundaries that determine which funds can route to the requested destination. The preview is always scoped to the relevant bridge domain(s). See Bridge domains reference at the end of this page.

Initiate a withdrawal

curl -X POST "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdraw" \
  -H "Authorization: Bearer $BRAID_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
    "chain": "ethereum",
    "token": "usdc",
    "withdrawalAmount": "65000",
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
  }'
FieldRequiredDescription
requestIdYesUUID v4 idempotency key
chainYesDestination chain
tokenYesDestination token
withdrawalAmountYesAmount as a string (max 6 decimal places for USDC, max 500,000)
destinationAddressYesRecipient address on the destination chain
Sandbox notes:
  • chain: "ethereum" refers to Ethereum Sepolia in sandbox.
  • Solana withdrawals are not currently supported (the endpoint expects an EVM destination address).

List withdrawals

curl -X GET "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawals?limit=25" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"
Query parameters:
ParamDescription
limitMax items (default 50, max 200)
cursorOpaque cursor from nextCursor
sortSort field (default createdAt)
orderdesc (default) or asc
createdAtGteISO-8601 timestamp filter
statusRepeatable status filter (e.g. status=processing&status=completed)

Fetch a withdrawal

curl -X GET "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"

Withdrawal statuses

Withdrawals move through these statuses:
StatusMeaning
pending_liquidityAccepted and reserved; sourcing hasn’t started yet
processingUnwinding/bridging/sourcing in progress
queued_for_customer_approvalReady for approval but blocked behind a prior approval on the same wallet+chain
pending_customer_approvalCustomer action required (Turnkey approval)
pending_broadcastSigned but not yet broadcast
broadcastedBroadcast submitted; finality pending
completedTerminal: all payout legs finished
partially_completedTerminal: some legs completed, others failed or were cancelled
failedTerminal: withdrawal failed
cancelledTerminal: withdrawal was cancelled

Payout legs

Each withdrawal includes a payouts array representing one or more payout legs. Payout legs track chain transaction hashes and leg-level progress.
Payout statusMeaning
planningLeg is being planned
waiting_for_prior_approvalBlocked behind a prior approval on the same wallet+chain
pending_customer_approvalCustomer approval required
pending_broadcastSigned, waiting to broadcast
broadcastedBroadcast submitted
processingIn progress
completedTerminal: success
failedTerminal: failed
cancelledTerminal: cancelled
skippedTerminal: leg not needed
A withdrawal is complete when withdrawal.status is completed and all payout legs are in a terminal state (completed or skipped).

Customer approvals (head-of-line gating)

Only one customer approval can be outstanding per portfolioWalletId + chain. If you submit multiple withdrawals quickly, later ones may enter queued_for_customer_approval until earlier approvals resolve. Withdrawal responses include:
  • customerApprovalState: "required" | "queued" | "not_required"
  • blockedBy: when queued, describes the blocking payout leg
For the full Turnkey approval lifecycle and automation patterns, see Transaction Approvals.

Cancel a withdrawal

Cancel an in-progress withdrawal:
curl -X POST "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID/cancel" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"
Cancellation is best-effort. If some payout legs have already completed, the withdrawal transitions to partially_completed rather than cancelled. You can also cancel or retry individual payout legs:
# Cancel a specific payout leg
curl -X POST "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID/payouts/$PAYOUT_ID/cancel" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"

# Retry a failed payout leg
curl -X POST "$BASE_URL/v2/portfolio-wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID/payouts/$PAYOUT_ID/retry" \
  -H "Authorization: Bearer $BRAID_API_TOKEN"

Timing expectations

Withdrawal time depends on which yield positions need to unwind and whether cross-chain bridging is required.
PositionUnwind timeNotes
Cash (USDC)InstantNo unwind needed
usdzInstant (PT0H)Convert to USDC on Arbitrum
syrupUsdcInstant (PT0H)ERC-4626 redemption on Ethereum
rlpUp to 24h (PT24H)Async Resolv exit window
Cross-chain delivery (CCTP) adds time on top of the unwind. Typical CCTP bridging is ~1 hour (p50), up to ~3 hours (max). For precise per-position estimates to a specific destination, use POST /v2/portfolio-wallets/withdrawal-time-estimates (see Yield Sources).

Withdrawal webhooks

Subscribe to these events for real-time withdrawal tracking:
  • portfolio_wallet.withdrawal.status_changed
  • portfolio_wallet.withdrawal.payout.status_changed
See Webhooks for registration and payload details.

Bridge domains reference

A bridge domain is a hard boundary for where liquidity can come from. The withdrawal engine never crosses bridge domain boundaries.

USDC unified (CCTP)

USDC on CCTP-supported chains forms a single usdc:unified domain:
  • Ethereum, Arbitrum, Base, Polygon, Optimism, Avalanche, Solana
A withdrawal to any of these chains can source USDC from any other chain in the domain (via CCTP bridging).

BSC (always isolated)

BSC is always isolated, even for tokens that unify elsewhere:
  • usdc:bsc, usdt:bsc

USDT (always per-chain)

USDT has no cross-chain unification:
  • usdt:ethereum, usdt:arbitrum, usdt:solana, etc.

Default (per-chain)

All other tokens are per-chain:
  • dai:ethereum, usdz:arbitrum, etc.

How cash fits in

Within a bridge domain, cash is always the first source of liquidity. Only if cash is insufficient will the system unwind yield positions in the same domain. The positions[] array in the withdrawal preview shows this ordering. Bridge domain IDs are always lowercase and intended to be stable, but use the withdrawal preview as the canonical source of truth for what’s available.