> ## Documentation Index
> Fetch the complete documentation index at: https://docs.derive.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfers & Withdrawals

> Move collateral between subaccounts, to other wallets, and on-chain, plus transferring positions.

Moving value in the Derive v3 API is always a **[signed action](/authentication/action-signing)**: your wallet (or a scoped [session key](/authentication/session-keys)) EIP-712-signs an `Action` envelope, and the protocol re-verifies the signature before applying it. Four methods cover the distinct destinations:

<CardGroup cols={2}>
  <Card title="private/transfer_spot" icon="arrow-right-arrow-left">
    Move collateral between two subaccounts you own.
  </Card>

  <Card title="private/transfer_spot_external" icon="paper-plane">
    Send collateral to a subaccount owned by a **different** wallet, bounded by your recipient allow-list.
  </Card>

  <Card title="private/withdraw" icon="building-columns">
    Withdraw collateral on-chain to an L1 recipient address.
  </Card>

  <Card title="private/transfer_positions" icon="layer-group">
    Move open positions between subaccounts, booked as an RFQ trade.
  </Card>
</CardGroup>

Each method needs a specific [protocol scope](/authentication/access-scopes) on the signing key, and each carries a `nonce`, `signer`, `signature`, and `signature_expiry_sec` built exactly as described in [Action signing](/authentication/action-signing). The action `module` for each flow is filled in by the server and does not appear on the wire — it is part of the signed struct hash and must match the deployment's module address in [Action signing](/authentication/action-signing#per-action-modules-and-data).

<Note>
  Amounts, prices, and fees are human decimals on the wire — decimal strings (e.g. `"100.5"`) or JSON numbers. The
  exception is `private/withdraw`, whose on-chain amount uses the asset's **native ERC-20 decimals** (see below).
</Note>

## Transfer collateral between your subaccounts

`private/transfer_spot` moves a spot balance from one of your subaccounts to another subaccount **you own** (existing, or a new one created in the same call). Positions are not moved by this method — use [`private/transfer_positions`](#transfer-positions-between-subaccounts) for that.

**Required scope:** `transfer:existing_subaccount` **or** `transfer:new_subaccount` (see [Access scopes](/authentication/access-scopes)).

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { DeriveClient } from '@derivexyz/derive-ts';

  const client = new DeriveClient({
    network: 'mainnet',
    wallet: process.env.PRIVATE_KEY!,
  });
  await client.connect(); // open the websocket
  await client.login(); // authenticate the session — required for private/\*

  // transferInternal resolves 'USDC' to its protocol asset, encodes and
  // EIP-712-signs the transfer action locally, then submits private/transfer_spot.
  const result = await client.spotTransfers.transferInternal({
    subaccountId: 9,
    toSubaccountId: 12,
    asset: 'USDC',
    amount: '250',
    maxFeeUsd: '1.5',
  });
  console.log(result.op_uuid, result.operation_id);

  await client.close();
  ```

  ```python Python (SDK) theme={null}
  import asyncio
  from decimal import Decimal

  from derive_py import WebSocketClient


  async def main():
      client = WebSocketClient.from_env()
      await client.connect()

      # transfer_spot resolves "USDC" to its protocol asset, encodes and
      # EIP-712-signs the transfer action locally, then submits private/transfer_spot.
      result = await client.collateral.transfer_spot(
          to_subaccount_id=12,
          asset_name="USDC",
          amount=Decimal("250"),
          max_fee_usd=Decimal("1.5"),
      )
      print(result.op_uuid, result.operation_id)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use bigdecimal::BigDecimal;
  use derive_rs::{Environment, WsClient, actions::SpotTransferArgs};

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      // The SDK resolves "USDC" to its protocol asset from the client's cache,
      // EIP-712-signs the transfer action, then submits private/transfer_spot.
      let args = SpotTransferArgs::builder()
          .subaccount_id(9)
          .to_subaccount_id(12)
          .new_subaccount_manager(0) // 0 = transfer into an existing subaccount
          .asset("USDC".to_string())
          .amount(BigDecimal::from(250))
          .max_fee_usd("1.5".parse()?)
          .build();

      let result = client.fund_movements().transfer_spot(args).await?;
      println!("{} {}", result.op_uuid, result.operation_id);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.derive.xyz/v3/private/transfer_spot \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: 0xYourWalletAddress" \
    -H "X-DeriveTimestamp: 1731000000000" \
    -H "X-DeriveSignature: 0x…session-signature" \
    -d '{
      "subaccount_id": 9,
      "to_subaccount_id": 12,
      "new_subaccount_manager": 0,
      "asset_name": "USDC",
      "sub_id": 0,
      "amount": "250",
      "max_fee_usd": "1.5",
      "nonce": "1731000000000123000",
      "signer": "0xYourWalletOrSessionKey",
      "signature": "0x...",
      "signature_expiry_sec": 1731000300
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/transfer_spot",
    "params": {
      "subaccount_id": 9,
      "to_subaccount_id": 12,
      "new_subaccount_manager": 0,
      "asset_name": "USDC",
      "sub_id": 0,
      "amount": "250",
      "max_fee_usd": "1.5",
      "nonce": "1731000000000123000",
      "signer": "0xYourWalletOrSessionKey",
      "signature": "0x...",
      "signature_expiry_sec": 1731000300
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "op_uuid": "b2c3...",
      "operation_id": 84213
    }
  }
  ```
</CodeGroup>

## Transfer positions between subaccounts

`private/transfer_positions` moves open positions from one subaccount to another. It is **booked as an RFQ-module trade**: both sides sign a transfer quote over the same legs, and the transfer clears at the agreed prices. The signing key needs a `transfer:*` scope covering the destination (`transfer:existing_subaccount`, `transfer:new_subaccount`, or `transfer:different_owner_subaccount`).

Both quotes sign the same legs hash; each side authorizes its own `max_fee`. The response returns the resulting `maker_quote` and `taker_quote`.

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { DeriveClient } from '@derivexyz/derive-ts';

  const client = new DeriveClient({
    network: 'mainnet',
    wallet: process.env.PRIVATE_KEY!,
  });
  await client.connect();
  await client.login();

  // The SDK resolves each leg's instrument, signs the maker quote and the
  // matching opposite-direction taker execute (both zero-fee), then submits
  // private/transfer_positions. The taker's 'sell' direction is derived here.
  const result = await client.positionTransfers.transferPositions({
    makerSubaccountId: 9,
    takerSubaccountId: 12,
    makerDirection: 'buy',
    legs: [
      { instrumentName: 'ETH-PERP', amount: '1', price: '0', direction: 'buy' },
    ],
  });
  console.log(result.maker_quote, result.taker_quote);

  await client.close();
  ```

  ```python Python (SDK) theme={null}
  import asyncio
  from decimal import Decimal

  from derive_py import WebSocketClient
  from derive_py.data_types import Direction, PositionTransfer


  async def main():
      client = WebSocketClient.from_env()
      await client.connect()

      # The SDK signs the maker quote and the matching opposite-direction taker
      # execute (both zero-fee), then submits private/transfer_positions.
      result = await client.positions.transfer(
          positions=[PositionTransfer("ETH-PERP", Decimal("1"))],
          direction=Direction.buy,
          to_subaccount=12,
      )
      print(result.maker_quote, result.taker_quote)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use bigdecimal::BigDecimal;
  use derive_rs::{
      Environment, WsClient,
      actions::TransferPositionsArgs,
      models::{Direction, PricedLegParamsAndResponse},
  };

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      let leg: PricedLegParamsAndResponse = PricedLegParamsAndResponse::builder()
          .instrument_name("ETH-PERP")
          .amount(BigDecimal::from(1))
          .price(BigDecimal::from(0))
          .direction(Direction::Buy)
          .try_into()?;

      // The SDK signs the maker quote and the matching opposite-direction taker
      // execute (both zero-fee), then submits private/transfer_positions.
      let args = TransferPositionsArgs::builder()
          .legs(vec![leg])
          .from_subaccount_id(9)
          .to_subaccount_id(12)
          .maker_direction(Direction::Buy)
          .max_fee(BigDecimal::from(0))
          .build();

      let result = client.fund_movements().transfer_positions(args).await?;
      println!("{:?} {:?}", result.maker_quote, result.taker_quote);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.derive.xyz/v3/private/transfer_positions \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: 0xYourWalletAddress" \
    -H "X-DeriveTimestamp: 1731000000000" \
    -H "X-DeriveSignature: 0x…session-signature" \
    -d '{
      "wallet": "0xYourWallet",
      "maker_params": {
        "direction": "buy",
        "legs": [{ "instrument_name": "ETH-PERP", "amount": "1", "price": "0", "direction": "buy" }],
        "max_fee": "1.5",
        "subaccount_id": 9,
        "nonce": "1731000000000321000",
        "signer": "0xYourWallet",
        "signature": "0x...",
        "signature_expiry_sec": 1731000300
      },
      "taker_params": {
        "direction": "sell",
        "legs": [{ "instrument_name": "ETH-PERP", "amount": "1", "price": "0", "direction": "buy" }],
        "max_fee": "1.5",
        "subaccount_id": 12,
        "nonce": "1731000000000322000",
        "signer": "0xYourWallet",
        "signature": "0x...",
        "signature_expiry_sec": 1731000300
      }
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/transfer_positions",
    "params": {
      "wallet": "0xYourWallet",
      "maker_params": {
        "direction": "buy",
        "legs": [
          {
            "instrument_name": "ETH-PERP",
            "amount": "1",
            "price": "0",
            "direction": "buy"
          }
        ],
        "max_fee": "1.5",
        "subaccount_id": 9,
        "nonce": "1731000000000321000",
        "signer": "0xYourWallet",
        "signature": "0x...",
        "signature_expiry_sec": 1731000300
      },
      "taker_params": {
        "direction": "sell",
        "legs": [
          {
            "instrument_name": "ETH-PERP",
            "amount": "1",
            "price": "0",
            "direction": "buy"
          }
        ],
        "max_fee": "1.5",
        "subaccount_id": 12,
        "nonce": "1731000000000322000",
        "signer": "0xYourWallet",
        "signature": "0x...",
        "signature_expiry_sec": 1731000300
      }
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "maker_quote": { "...": "..." },
      "taker_quote": { "...": "..." }
    }
  }
  ```
</CodeGroup>

## Transfer collateral to another wallet

`private/transfer_spot_external` sends collateral to a subaccount owned by a **different** wallet. The destination owner must be on the sender's **whitelisted-recipient allow-list**, and the signing key needs `transfer:different_owner_subaccount`.

### Managing the recipient allow-list

Withdrawals or transfers to external subaccounts can only reach wallets you have explicitly whitelisted.

Manage the list with `private/update_whitelisted_recipients`. Permissions that can modify this list are:

* owner
* `admin` scoped session key

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { DeriveClient } from '@derivexyz/derive-ts';

  const client = new DeriveClient({
    network: 'mainnet',
    wallet: process.env.PRIVATE_KEY!,
  });
  await client.connect();
  await client.login();

  // The resulting allow-list is (current ∪ add) \ remove; the wallet is taken
  // from the signing credentials. This is itself an admin-scoped signed action.
  const whitelist = await client.spotTransfers.updateWhitelistedRecipients({
    add: ['0xRecipientA', '0xRecipientB'],
    remove: ['0xOldRecipient'],
  });
  console.log(whitelist.whitelisted_recipients);

  await client.close();
  ```

  ```python Python (SDK) theme={null}
  import asyncio

  from derive_py import WebSocketClient


  async def main():
      client = WebSocketClient.from_env()
      await client.connect()

      # The resulting allow-list is (current | add) - remove; the wallet is taken
      # from the signing credentials. This is itself an admin-scoped signed action.
      whitelist = await client.account.update_whitelisted_recipients(
          add=["0xRecipientA", "0xRecipientB"],
          remove=["0xOldRecipient"],
      )
      print(whitelist.whitelisted_recipients)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use derive_rs::{Environment, WsClient, models::UpdateWhitelistedRecipientsRequest};

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      // The resulting allow-list is (current | add) \ remove. This is an
      // admin-scoped signed action, so nonce/signature must be supplied.
      let params = UpdateWhitelistedRecipientsRequest::builder()
          .wallet(std::env::var("DERIVE_WALLET")?)
          .add(vec!["0xRecipientA".to_string(), "0xRecipientB".to_string()])
          .remove(vec!["0xOldRecipient".to_string()])
          .try_into()?;

      let whitelist = client
          .rpc()
          .transfers_withdrawals()
          .update_whitelisted_recipients(params)
          .await?;
      println!("{:?}", whitelist.whitelisted_recipients);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.derive.xyz/v3/private/update_whitelisted_recipients \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: 0xYourWalletAddress" \
    -H "X-DeriveTimestamp: 1731000000000" \
    -H "X-DeriveSignature: 0x…session-signature" \
    -d '{
      "wallet": "0xYourWallet",
      "add": ["0xRecipientA", "0xRecipientB"],
      "remove": ["0xOldRecipient"],
      "nonce": "1731000000000456000",
      "signer": "0xYourWallet",
      "signature": "0x...",
      "signature_expiry_sec": 1731000600
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/update_whitelisted_recipients",
    "params": {
      "wallet": "0xYourWallet",
      "add": ["0xRecipientA", "0xRecipientB"],
      "remove": ["0xOldRecipient"],
      "nonce": "1731000000000456000",
      "signer": "0xYourWallet",
      "signature": "0x...",
      "signature_expiry_sec": 1731000600
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "op_uuid": "c4d5...",
      "operation_id": 84220,
      "whitelisted_recipients": ["0xRecipientA", "0xRecipientB"]
    }
  }
  ```
</CodeGroup>

## Withdraw

`private/withdraw` removes collateral from a subaccount and settles it to an **Ethereum L1 recipient**. It is a signed action requiring the `withdraw` scope.

Withdrawals signed by a session key must go to an address on the owner's **whitelisted-recipient allow-list**, including the owner wallet itself. See [Managing the recipient allow-list](#managing-the-recipient-allow-list) above.

<Warning>
  `amount_in_underlying` is denominated in the asset's **native ERC-20 decimals** (e.g. 6 for USDC), **not** the decimal
  convention used by the transfer methods above. Match the on-chain token's decimals exactly.
</Warning>

<Info>
  `recipient` is optional and **defaults to the account's owner wallet** — not the signer, and not the subaccount. The
  signature commits to it, so the server cannot redirect a payout: a `recipient` disagreeing with what you signed fails
  signature verification rather than being honored.

  A session key may only pay out to an address on the owner's whitelist — **including the owner wallet itself**, which is
  not whitelisted implicitly. Whitelist it before granting a key the `withdraw` scope, or the withdrawal is rejected with
  `RecipientNotWhitelisted`. The owner and `admin`-scoped keys skip the whitelist entirely.
</Info>

<CodeGroup>
  ```typescript TypeScript (SDK) theme={null}
  import { DeriveClient } from '@derivexyz/derive-ts';

  const client = new DeriveClient({
    network: 'mainnet',
    wallet: process.env.PRIVATE_KEY!,
  });
  await client.connect();
  await client.login();

  // Pass the amount in HUMAN units ("1000" = 1000 USDC); the SDK looks up the
  // token's native ERC-20 decimals and scales the signed amount_in_underlying.
  const result = await client.withdrawals.withdraw({
    subaccountId: 9,
    asset: 'USDC',
    amount: '1000',
    maxFeeUsd: '1.5',
    forceBatch: false,
    // Optional. Defaults to the owner wallet; any other address must be
    // whitelisted unless the owner or an admin-scoped key signs.
    recipient: '0xYourL1Address',
  });
  console.log(result.op_uuid, result.operation_id);

  await client.close();
  ```

  ```python Python (SDK) theme={null}
  import asyncio
  from decimal import Decimal

  from derive_py import WebSocketClient


  async def main():
      client = WebSocketClient.from_env()
      await client.connect()

      # Pass the amount in HUMAN units ("1000" = 1000 USDC); the SDK looks up the
      # token's native ERC-20 decimals and scales the signed amount.
      # The payout goes to the owner wallet unless a recipient is given.
      result = await client.active_subaccount.withdraw(
          asset_name="USDC",
          amount=Decimal("1000"),
          max_fee_usd=Decimal("1.5"),
          force_batch=False,
      )
      print(result.op_uuid, result.operation_id)

      await client.disconnect()


  asyncio.run(main())
  ```

  ```rust Rust (SDK) theme={null}
  use bigdecimal::BigDecimal;
  use derive_rs::{Environment, WsClient, actions::WithdrawArgs};

  #[tokio::main]
  async fn main() -> Result<(), Box<dyn std::error::Error>> {
      let client = WsClient::new(
          Environment::Mainnet,
          Some(std::env::var("DERIVE_PRIVATE_KEY")?),
          Some(std::env::var("DERIVE_WALLET")?),
          Some(9),
      )
      .await?;
      client.login().await?;

      // amount is in HUMAN units; the SDK scales it by the token's native
      // ERC-20 decimals when signing. The payout goes to recepient_address.
      let args = WithdrawArgs::builder()
          .asset("USDC".to_string())
          .amount(BigDecimal::from(1000))
          .max_fee_usd("1.5".parse()?)
          .recepient_address(std::env::var("DERIVE_WALLET")?.parse()?)
          .force_batch(false)
          .build();

      let result = client.fund_movements().withdraw(args).await?;
      println!("{} {}", result.op_uuid, result.operation_id);
      Ok(())
  }
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.derive.xyz/v3/private/withdraw \
    -H "Content-Type: application/json" \
    -H "X-DeriveWallet: 0xYourWalletAddress" \
    -H "X-DeriveTimestamp: 1731000000000" \
    -H "X-DeriveSignature: 0x…session-signature" \
    -d '{
      "subaccount_id": 9,
      "asset_name": "USDC",
      "amount_in_underlying": "1000000000",
      "force_batch": false,
      "max_fee_usd": "1.5",
      "recipient": "0xYourL1Address",
      "nonce": "1731000000000789000",
      "signer": "0xYourWalletOrSessionKey",
      "signature": "0x...",
      "signature_expiry_sec": 1731000300
    }'
  ```

  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/withdraw",
    "params": {
      "subaccount_id": 9,
      "asset_name": "USDC",
      "amount_in_underlying": "1000000000",
      "force_batch": false,
      "max_fee_usd": "1.5",
      "recipient": "0xYourL1Address",
      "nonce": "1731000000000789000",
      "signer": "0xYourWalletOrSessionKey",
      "signature": "0x...",
      "signature_expiry_sec": 1731000300
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "op_uuid": "d6e7...",
      "operation_id": 84231
    }
  }
  ```
</CodeGroup>

<Tip>
  `public/withdraw_debug` returns the EIP-712-encoded data and hashes for a withdraw action, so you can byte-compare
  against your local signing to diagnose rejected signatures. It is a debugging aid, not a required step — see [Action
  signing](/authentication/action-signing) for the full set of `*_debug` signing-preview helpers.
</Tip>


## Related topics

- [New features](/migrating/new-features.md)
- [Migration skill for your coding agent](/migrating/breaking-changes.md)
- [Programmatic Onboarding](/getting-started/depositing.md)
