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

# Transfers between exchanges

> Move USDC between exchanges and your Arbitrum wallet

Transfers route USDC between Hyperliquid, Lighter, and your Arbitrum wallet. The endpoint you start with depends on where the funds are leaving from:

* **From Hyperliquid or Lighter** — [`POST /transfer/prepare`](/docs/api-reference/transfer/prepare-a-cross-exchange-transfer) returns data to sign, then [`POST /transfer/execute`](/docs/api-reference/transfer/execute-a-prepared-transfer) hands the signature back to VOOI.
* **From your Arbitrum wallet** — [`POST /transfer/arbitrum`](/docs/api-reference/transfer/prepare-an-arbitrum-to-exchange-transfer) returns ready-to-broadcast Arbitrum calldata. You submit it on-chain yourself; there is no execute step.

<Info>
  **Prerequisites:** an active VOOI API token and the source venue connected. See [API Tokens](/docs/tokens) and [Use your own keys](/docs/exchanges) if you don't have them yet.
</Info>

## Supported routes

| Source | Destination | Endpoint |
| - | - | - |
| Hyperliquid or Lighter | Hyperliquid, Lighter, or your Arbitrum wallet (anywhere except the source) | [`POST /transfer/prepare`](/docs/api-reference/transfer/prepare-a-cross-exchange-transfer) + [`POST /transfer/execute`](/docs/api-reference/transfer/execute-a-prepared-transfer) |
| Your Arbitrum wallet | Hyperliquid or Lighter | [`POST /transfer/arbitrum`](/docs/api-reference/transfer/prepare-an-arbitrum-to-exchange-transfer) |

Every other venue — Aster, Extended, Robinhood, Binance, Bybit, MEXC, and Ondo — is neither a supported source nor destination for transfers. Use [Deposits](/docs/deposits) to fund them and [Withdrawals](/docs/withdrawals) to take funds out, or [Extended withdrawals](/docs/withdraw-extended) for Extended. Binance, Bybit, and MEXC expose no deposits or withdrawals through VOOI at all — move funds through those venues directly.

## From Hyperliquid or Lighter

### 1. Prepare

```bash theme={null}
POST https://perps-api.vooi.io/transfer/prepare
Authorization: Bearer <token>
Content-Type: application/json

{ "source": "hyperliquid", "dest": "lighter", "amount": "25" }
```

By default funds go to your own registered wallet — the address VOOI has on file for you. To send to a different wallet — for example, paying out an Arbitrum withdrawal to an address other than the registered one — include `address`.

The response's `transferDataToSign` is shaped by `source` (see [step 2](#2-sign)), and it always includes a `quoteDataToSign` you must also sign — on every transfer, including ones going to your own registered wallet. Full schema lives in the [API reference](/docs/api-reference/transfer/prepare-a-cross-exchange-transfer).

**Fees.** `fee` and `exchangeFee` are both USDC decimal values. `fee` is the total deducted from the transfer: your source balance drops by the amount you requested, and the destination receives `amount − fee`. `exchangeFee` is the portion of `fee` retained by the source venue — it's already included in `fee`, not added on top. The remainder, `fee − exchangeFee`, is the routing fee.

### 2. Sign

The shape of `transferDataToSign` depends on `source`:

#### Source: Hyperliquid

An EIP-712 envelope for Hyperliquid's `usdSend` action:

```json theme={null}
{
  "domain": {
    "chainId": 42161,
    "name": "HyperliquidSignTransaction",
    "verifyingContract": "0x0000000000000000000000000000000000000000",
    "version": "1"
  },
  "primaryType": "HyperliquidTransaction:UsdSend",
  "types": {
    "HyperliquidTransaction:UsdSend": [
      { "name": "hyperliquidChain", "type": "string" },
      { "name": "destination", "type": "string" },
      { "name": "amount", "type": "string" },
      { "name": "time", "type": "uint64" }
    ]
  },
  "message": { "hyperliquidChain": "Mainnet", "destination": "0x...", "amount": "25", "time": 1744502400000 }
}
```

`time` is the only `uint64` field — encode it as an unsigned 64-bit integer before signing:

```typescript theme={null}
const transferSignature = await account.signTypedData({
  domain: transferDataToSign.domain,
  types: transferDataToSign.types,
  primaryType: transferDataToSign.primaryType,
  message: { ...transferDataToSign.message, time: BigInt(transferDataToSign.message.time) },
});
```

#### Source: Lighter

A plain-text string. Sign it as a plain personal-sign message, not as EIP-712 typed data:

```typescript theme={null}
const transferSignature = await account.signMessage({ message: transferDataToSign });
```

#### Sign the quote

`prepare` always returns a `quoteDataToSign` — an EIP-712 envelope authorizing the transfer quote on the destination side. Sign it in addition to `transferDataToSign`:

```typescript theme={null}
const quoteSignature = await account.signTypedData({
  domain: quoteDataToSign.domain,
  types: quoteDataToSign.types,
  primaryType: quoteDataToSign.primaryType,
  message: quoteDataToSign.message,
});
```

### 3. Submit

Post both signatures back. `quoteSignature` is required on every transfer — omitting it returns a `400`.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://perps-api.vooi.io/transfer/execute \
    -H "Authorization: Bearer <token>" \
    -H "Content-Type: application/json" \
    -d '{ "transferId": "...", "transferSignature": "0x...", "quoteSignature": "0x..." }'
  ```

  ```typescript TypeScript theme={null}
  await fetch('https://perps-api.vooi.io/transfer/execute', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ transferId, transferSignature, quoteSignature }),
  });
  ```
</CodeGroup>

VOOI submits the signed payload to the source venue on your behalf — you don't broadcast it yourself.

## From your Arbitrum wallet

```bash theme={null}
POST https://perps-api.vooi.io/transfer/arbitrum
Authorization: Bearer <token>
Content-Type: application/json

{ "address": "0xYourArbitrumWallet", "amount": "25", "exchange": "hyperliquid" }
```

`address` here is the source wallet (the one your signer controls) — opposite role from the `address` field on `/transfer/prepare`. `exchange` is the destination, `hyperliquid` or `lighter`. The response carries `transferId`, `fee`, and ready-to-broadcast `calldata`:

```json theme={null}
{
  "transferId": "1234",
  "fee": 0.5,
  "calldata": { "chainId": 42161, "to": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "data": "0xa9059cbb..." }
}
```

`data` is ERC-20 `transfer()` calldata with a short reference suffix appended — VOOI uses the suffix to match the on-chain transaction back to this `transferId`. Submit the transaction as-is; don't modify or truncate `data`, and don't call `/transfer/execute`.

```typescript theme={null}
const txHash = await wallet.sendTransaction({
  chainId: calldata.chainId,
  to: calldata.to,
  data: calldata.data,
});
```

You pay Arbitrum gas in ETH separately.

## Status and retry

Transfers move through `created → pending → sent → paid → confirmed`, or land on `failed` / `refunded`. Poll [`GET /transfer/status`](/docs/api-reference/transfer/get-the-status-of-a-margin-transfer-by-id) with the `transferId` to follow the progression; [`GET /transfer/history`](/docs/api-reference/transfer/get-paginated-margin-transfer-history) paginates the same records.

`transferId` is single-use. If the execute call times out, retry with the same `transferId` and signatures — the status endpoint is authoritative for the final outcome. Only prepare a new transfer after confirming the previous one failed.

A Hyperliquid or Lighter transfer that stays in `created` for more than 24 hours — meaning it was never executed — moves to `failed`. For an Arbitrum-wallet transfer, re-broadcasting the same signed transaction is a no-op once it has been mined; on network errors, query the status endpoint rather than starting over.

## Next steps

<CardGroup cols={2}>
  <Card title="Deposit into an exchange" icon="arrow-down" href="/docs/deposits">
    Fund a venue from an external wallet
  </Card>

  <Card title="Start trading" icon="bolt" href="/docs/trading-orders">
    Place your first order
  </Card>
</CardGroup>


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