> ## Documentation Index
> Fetch the complete documentation index at: https://v3.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}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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 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`.

The parameters mirror `private/transfer_spot`, with one addition and one change in meaning:

<ParamField path="recipient_address" type="string" required>
  Owner (wallet) of the destination account — must be whitelisted.
</ParamField>

<ParamField path="to_subaccount_id" type="integer" required>
  Recipient's existing destination subaccount. `0` ⇒ create a new subaccount for the recipient.
</ParamField>

<ParamField path="new_subaccount_manager" type="integer" required>
  Manager id for the new subaccount when `to_subaccount_id == 0`.
</ParamField>

All other fields (`subaccount_id`, `asset_name`, `sub_id`, `amount`, `max_fee_usd`, `nonce`, `signer`, `signature`, `signature_expiry_sec`) are as in `private/transfer_spot`, and the response returns `op_uuid` and `operation_id`.

### Managing the recipient allow-list

External transfers can only reach wallets you have explicitly whitelisted. Manage the list with `private/update_whitelisted_recipients`, itself a signed action requiring the `admin` scope.

The action carries an **add set** and a **remove set**; the new allow-list is `(current ∪ add) \ remove`. Removals are applied after additions.

<ParamField path="wallet" type="string" required>
  Wallet whose allow-list is being updated; becomes the action owner.
</ParamField>

<ParamField path="add" type="string[]" required>
  Recipient wallet addresses to add.
</ParamField>

<ParamField path="remove" type="string[]" required>
  Recipient wallet addresses to remove.
</ParamField>

<ParamField path="nonce" type="string" required>
  Strictly-increasing anti-replay nonce.
</ParamField>

<ParamField path="signer" type="string" required>
  Address that produced `signature`.
</ParamField>

<ParamField path="signature" type="string" required>
  0x-prefixed 65-byte EIP-712 signature over the update action.
</ParamField>

<ParamField path="signature_expiry_sec" type="integer" required>
  Unix seconds after which the signed action is rejected.
</ParamField>

<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}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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 collateral on-chain

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

<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>

<ParamField path="subaccount_id" type="integer" required>
  Subaccount the collateral is withdrawn from.
</ParamField>

<ParamField path="asset_name" type="string" required>
  Collateral asset to withdraw.
</ParamField>

<ParamField path="amount_in_underlying" type="string" required>
  Amount in the asset's native ERC-20 decimals.
</ParamField>

<ParamField path="force_batch" type="boolean" required>
  Whether to force the withdrawal into a settlement batch.
</ParamField>

<ParamField path="max_fee_usd" type="string" required>
  Maximum sequencer fee, in USD, the signer authorizes.
</ParamField>

<ParamField path="nonce" type="string" required>
  Strictly-increasing anti-replay nonce.
</ParamField>

<ParamField path="signer" type="string" required>
  Address that produced `signature`.
</ParamField>

<ParamField path="signature" type="string" required>
  0x-prefixed 65-byte EIP-712 signature over the withdraw action.
</ParamField>

<ParamField path="signature_expiry_sec" type="integer" required>
  Unix seconds after which the signed action is rejected.
</ParamField>

<Info>
  The withdraw request does not carry an explicit L1 recipient field; the on-chain recipient in the signed action is
  resolved server-side (e.g. from the wallet's registered L1 address). Confirm the recipient resolution for your target
  deployment against the **API Reference** tab before mainnet use.
</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.
  // The exchange pays out to whichever address signs the action.
  const result = await client.withdrawals.withdraw({
    subaccountId: 9,
    asset: 'USDC',
    amount: '1000',
    maxFeeUsd: '1.5',
    forceBatch: false,
  });
  console.log(result.op_uuid, result.operation_id);

  await client.close();
  ```

  ```python Python (SDK) theme={null}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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",
      "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",
      "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>

## 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`).

The request carries a `maker_params` and a `taker_params` object — each a signed transfer quote — plus `wallet`:

<ParamField path="wallet" type="string" required>
  Wallet initiating the position transfer.
</ParamField>

<ParamField path="maker_params" type="object" required>
  Maker-side signed transfer quote: `direction`, `legs`, `max_fee`, `subaccount_id`, and the `nonce` / `signer` /
  `signature` / `signature_expiry_sec` signing fields.
</ParamField>

<ParamField path="taker_params" type="object" required>
  Taker-side signed transfer quote over the same `legs`, with its own `max_fee` and signing fields.
</ParamField>

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}
  # Coming soon.
  ```

  ```rust Rust (SDK) theme={null}
  // Coming soon.
  ```

  ```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>

<Info>
  The `legs` fields for a transfer quote follow the RFQ leg shape; confirm the precise per-leg parameters against the
  the **API Reference** tab. See [RFQ](/trading/rfq) for the underlying quote-signing mechanics.
</Info>


## Related topics

- [Update a wallet's whitelisted transfer recipients](/api-reference/transfers-&-withdrawals/update-a-wallets-whitelisted-transfer-recipients.md)
- [Transfer a spot asset to another owner's subaccount](/api-reference/transfers-&-withdrawals/transfer-a-spot-asset-to-another-owners-subaccount.md)
- [Transfer positions between two subaccounts by RFQ](/api-reference/transfers-&-withdrawals/transfer-positions-between-two-subaccounts-by-rfq.md)
