> ## 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 bound to no subaccounts 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 unbounded 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>

## Forwarded withdrawals

Some tokens come to Derive over a bridge from their home chain. You can withdraw these tokens back to the
home chain in one step. The withdrawal goes to a **withdraw address**: an Ethereum address bound to one
recipient on one destination chain. When the payout lands there, Derive bridges it to the recipient. Derive
pays the bridge fee. You pay only the normal withdrawal fee.

| Environment | Destination | `chain_id` | Tokens | Bridge |
| - | - | - | - | - |
| Mainnet | HyperEVM | `999` | HYPE | LayerZero |
| Mainnet | HyperEVM | `999` | kHYPE | CCIP |
| Mainnet | Base | `8453` | cbBTC | LayerZero |
| Mainnet | Flare | `14` | FXRP | LayerZero |
| Mainnet | Solana | `101` | wSOL, jitoSOL | LayerZero |
| Testnet | HyperEVM testnet | `998` | HYPE, kHYPE | LayerZero |
| Testnet | Coston2 | `114` | FXRP | LayerZero |
| Testnet | Solana devnet | `103` | wSOL, jitoSOL | LayerZero |

`chain_id` is the destination's chain id. Solana uses `101` (mainnet-beta) and `103` (devnet); every other
`chain_id` is an EVM chain. A withdraw address forwards only the tokens in the `routes` list that
`private/register_withdraw_address` returns. Send it nothing else: `private/withdraw` does not check the token,
and a token the address doesn't forward is held there (`held`, `no_route`) until Derive recovers it and
refunds your wallet by hand.

### Flow

<Steps>
  <Step title="Get the withdraw address">
    Call `private/register_withdraw_address` with the destination `chain_id` and `recipient`.
  </Step>

  <Step title="Whitelist it (scoped session keys only)">
    Skip this step if the owner wallet, or an `admin` key bound to no subaccounts, signs the withdrawal. A scoped
    session key can only withdraw to a whitelisted address, for EVM and Solana destinations alike. The owner first
    adds the withdraw address with `private/update_whitelisted_recipients`, or `private/withdraw` fails with 11033
    `transfer_not_whitelisted` (the protocol's `RecipientNotWhitelisted`).
  </Step>

  <Step title="Withdraw to it">
    Call `private/withdraw` with `recipient` set to the withdraw address. The signature commits to the recipient,
    so get the address before you sign.
  </Step>

  <Step title="Track it">
    `private/get_withdrawal_history` shows the forward under `forward` (see below).
  </Step>
</Steps>

<Info>
  Withdrawals signed by the owner wallet or by an `admin` key bound to no subaccounts need no whitelisting. Every
  other session key needs the withdraw address on `whitelisted_recipients` first. The `whitelisted` flag in
  `private/get_withdraw_addresses` shows which addresses are ready.
</Info>

### Register a withdraw address

`private/register_withdraw_address` returns the withdraw address for one `(recipient, chain_id)` and starts
forwarding for it. It is idempotent: the same params return the same address. The caller must be authenticated
as `wallet`.

<ParamField path="wallet" type="string" required>
  Your account wallet. It must match the authenticated wallet.
</ParamField>

<ParamField path="chain_id" type="integer" required>
  Destination chain, from the table above.
</ParamField>

<ParamField path="recipient" type="string" required>
  Recipient on the destination chain.

  * **EVM chain:** must be your account `wallet`. Forwards go to the same address on the home chain.
  * **Solana:** a base58 wallet public key. It must be on the ed25519 curve, so a program address or a token
    account is rejected: the bridge credits the recipient's token account itself. Derive screens the address
    before it returns a withdraw address.
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "private/register_withdraw_address",
    "params": {
      "wallet": "0xYourWalletAddress",
      "chain_id": 101,
      "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "withdraw_address": "0xWithdrawAddress",
      "chain_id": 101,
      "recipient": "0x66a6b9d3...",
      "recipient_display": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "routes": [
        { "symbol": "wSOL", "token": "0xb5EaD284CD00fD0EB37E84327a4ee781172A07D6", "bridge_type": "native-oft" },
        { "symbol": "jitoSOL", "token": "0xc2908Db337AA16fB22b01bd4e8fF91460b23fF4B", "bridge_type": "native-oft" }
      ]
    }
  }
  ```
</CodeGroup>

`recipient` is the 32-byte form the bridge uses. `recipient_display` is the address as you gave it, normalised.

| Code | Name | When it fires |
| - | - | - |
| 11046 | `withdraw_destination_unsupported` | `chain_id` has no forwarding route |
| 11047 | `invalid_withdraw_recipient` | `recipient` is not a valid address on that chain, or an EVM recipient is not `wallet` |
| 403 | `forbidden` | The Solana recipient failed address screening |
| -32603 | `internal_error` | Address screening is unavailable. Retry later. |
| -32000 | `rate_limit_exceeded` | Too many new withdraw addresses were registered in the last minute. Retry later. |

### List your withdraw addresses

`private/get_withdraw_addresses {wallet}` returns your EVM withdraw addresses (their recipient is your wallet)
and every withdraw address on your `whitelisted_recipients`, oldest first. A Solana withdraw address you
registered but never whitelisted is not listed: keep the `register_withdraw_address` result for it, or call
`register_withdraw_address` again (it returns the same address).

```json Response theme={null}
{
  "withdraw_addresses": [
    {
      "withdraw_address": "0xWithdrawAddress",
      "chain_id": 101,
      "address_kind": "solana",
      "recipient": "0x66a6b9d3...",
      "recipient_display": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "whitelisted": true
    }
  ]
}
```

`address_kind` is `evm` or `solana`. `whitelisted` tells you whether a scoped session key can withdraw to the
address now. Owner and unbounded `admin` signatures don't need it.

### Track a forwarded withdrawal

Each `private/get_withdrawal_history` entry whose `recipient` is a withdraw address has a `forward` object:

| Field | Type | Meaning |
| - | - | - |
| `status` | string | Where the forward is (below) |
| `hold_reason` | string \| null | Set only when `status` is `held`: `no_route`, `route_unavailable`, `external_only` or `dust` |
| `chain_id` | integer | Destination chain |
| `recipient` | string | Recipient as registered |
| `rail` | string \| null | `layerzero` or `ccip`; null until a forward is sent |
| `forward_tx` | string \| null | The Ethereum tx that sent the forward, or the refund recovery |
| `message_id` | string \| null | LayerZero GUID or CCIP message id |
| `destination_tx` | string \| null | Destination-chain tx, once delivered |
| `amount_forwarded` | string \| null | This withdrawal's part of the forwarded amount; null until a forward is sent |

| `status` | Meaning |
| - | - |
| `awaiting_payout` | The withdrawal is not fully paid out to the withdraw address yet |
| `pending` | Paid out; the forward is not sent yet |
| `held` | Not forwarding; see `hold_reason`. `route_unavailable` is temporary: show it as delayed. |
| `in_flight` | Sent on the bridge, not delivered yet |
| `delivered` | Arrived; `destination_tx` is set |
| `failed` | The bridge reported a failure. A retry can still deliver it, so keep polling. |
| `recovered` | Pulled back for a manual refund |

A token amount below the bridge's smallest unit stays at the withdraw address and goes with the next
forward, so the amount delivered can be slightly lower than `amount_forwarded`.

Explorer links: LayerZero `https://layerzeroscan.com/tx/<forward_tx>` (testnet
`https://testnet.layerzeroscan.com/tx/<forward_tx>`), CCIP `https://ccip.chain.link/msg/<message_id>`.


## Related topics

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.