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

# Virtual Users

> List virtual users and retrieve their portfolio holdings

Manage and query portfolios for virtual users — abstracted identities that represent balance-tracking or CEX-linked accounts. Virtual user endpoints follow the same patterns as [Portfolio](/api/endpoints/portfolio) but use `virtual:<id>` addresses.

<Warning>
  **Pro subscription required.** Virtual users are a Pro feature. Create and manage them in the [Octav Pro](https://pro.octav.fi) app — the API provides read-only access.
</Warning>

<Note>
  **Interactive Playground:** Test these endpoints in the [API Playground](/api-reference/virtual-users). Get your API key at [data.octav.fi](https://data.octav.fi/)
</Note>

<Info>
  **Cost:** 1 credit per call (list) · 1 credit per virtual user address (portfolio)
</Info>

***

## List Virtual Users

Returns all virtual users belonging to the authenticated API user.

### Endpoint

<CodeGroup>
  ```bash Request theme={null}
  GET https://api.octav.fi/v1/virtual-users
  ```

  ```bash Example theme={null}
  curl -X GET "https://api.octav.fi/v1/virtual-users" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</CodeGroup>

### Parameters

No query parameters required — returns all virtual users for the authenticated user.

### Response

Returns an array of virtual user objects.

<ResponseField name="address" type="string">
  Virtual user identifier in `virtual:<id>` format. Use this value in the portfolio endpoint's `addresses` parameter.
</ResponseField>

<ResponseField name="type" type="string">
  The virtual user type (e.g. `BALANCE`, `CEX`)
</ResponseField>

<ResponseField name="label" type="string">
  User-defined label for the virtual user
</ResponseField>

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.octav.fi/v1/virtual-users" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://api.octav.fi/v1/virtual-users',
    {
      headers: {
        'Authorization': `Bearer ${apiKey}`
      }
    }
  );

  const virtualUsers = await response.json();
  virtualUsers.forEach(user => {
    console.log(`${user.label}: ${user.address} (${user.type})`);
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      'https://api.octav.fi/v1/virtual-users',
      headers={'Authorization': f'Bearer {api_key}'}
  )

  virtual_users = response.json()
  for user in virtual_users:
      print(f"{user['label']}: {user['address']} ({user['type']})")
  ```

  ```typescript TypeScript theme={null}
  interface VirtualUser {
    address: string;
    type: string;
    label: string;
  }

  const response = await fetch(
    'https://api.octav.fi/v1/virtual-users',
    {
      headers: {
        'Authorization': `Bearer ${apiKey}`
      }
    }
  );

  const virtualUsers: VirtualUser[] = await response.json();
  virtualUsers.forEach(user => {
    console.log(`${user.label}: ${user.address} (${user.type})`);
  });
  ```
</CodeGroup>

### Example Response

<Accordion title="View Full Response" icon="code">
  ```json theme={null}
  [
    {
      "address": "virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "type": "BALANCE",
      "label": "My Virtual Portfolio"
    },
    {
      "address": "virtual:f9e8d7c6-b5a4-3210-fedc-ba0987654321",
      "type": "CEX",
      "label": "Exchange Account"
    }
  ]
  ```
</Accordion>

***

## Virtual Users Portfolio

Fetch portfolios for one or more virtual users. Works identically to [GET /v1/portfolio](/api/endpoints/portfolio) but uses virtual user addresses instead of wallet addresses.

### Endpoint

<CodeGroup>
  ```bash Request theme={null}
  GET https://api.octav.fi/v1/virtual-users/portfolio
  ```

  ```bash Example theme={null}
  curl -X GET "https://api.octav.fi/v1/virtual-users/portfolio?addresses=virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```
</CodeGroup>

### Parameters

<ParamField query="addresses" type="string" required>
  Comma-separated virtual user addresses (from the list endpoint). Format: `virtual:<id>`. Max 10.

  ```
  addresses=virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890,virtual:f9e8d7c6-b5a4-3210-fedc-ba0987654321
  ```
</ParamField>

<ParamField query="aggregated" type="boolean" default="false">
  Return a single reduced portfolio across all virtual users

  When `true`, returns a single-element array with merged holdings from all specified virtual users
</ParamField>

<ParamField query="waitForSync" type="boolean" default="false">
  Wait for fresh data if cache is stale

  * `false`: Return cached data immediately (recommended)
  * `true`: Wait for sync if data is older than 1 minute
</ParamField>

<ParamField query="includeImages" type="boolean" default="false">
  Include image URLs for chains, assets, and protocols

  Useful for displaying logos in your application UI
</ParamField>

<ParamField query="includeExplorerUrls" type="boolean" default="false">
  Include blockchain explorer URLs for assets and transactions

  Links to Etherscan, Arbiscan, etc. for easy navigation
</ParamField>

### Response

Same portfolio schema as [GET /v1/portfolio](/api/endpoints/portfolio). Returns an array of portfolio objects (one per virtual user), or a single-element array if `aggregated=true`.

### Portfolio Object

<ResponseField name="address" type="string">
  The virtual user address (`virtual:<id>`)
</ResponseField>

<ResponseField name="networth" type="string">
  Total portfolio net worth in USD
</ResponseField>

<ResponseField name="cashBalance" type="string">
  Available cash balance
</ResponseField>

<ResponseField name="dailyIncome" type="string">
  Income generated today
</ResponseField>

<ResponseField name="dailyExpense" type="string">
  Expenses incurred today
</ResponseField>

<ResponseField name="fees" type="string">
  Total fees in native asset
</ResponseField>

<ResponseField name="feesFiat" type="string">
  Total fees in USD
</ResponseField>

<ResponseField name="lastUpdated" type="string">
  Last sync timestamp (milliseconds since epoch)
</ResponseField>

<ResponseField name="openPnl" type="string">
  Unrealized profit/loss (if available, otherwise "N/A")
</ResponseField>

<ResponseField name="closedPnl" type="string">
  Realized profit/loss (if available, otherwise "N/A")
</ResponseField>

<ResponseField name="totalCostBasis" type="string">
  Total cost basis of holdings (if available, otherwise "N/A")
</ResponseField>

<ResponseField name="assetByProtocols" type="object">
  Assets organized by protocol (wallet, lending, staking, etc.)

  Each protocol contains:

  * `key`: Protocol identifier
  * `name`: Protocol display name
  * `value`: Total value in USD
  * `assets[]`: Array of asset holdings
</ResponseField>

<ResponseField name="chains" type="object">
  Assets organized by blockchain

  Each chain contains:

  * `key`: Chain identifier (e.g., "ethereum", "arbitrum")
  * `name`: Chain display name
  * `value`: Total value on this chain
  * `protocols[]`: Protocols with positions on this chain
</ResponseField>

### Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.octav.fi/v1/virtual-users/portfolio?addresses=virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890&aggregated=true&includeImages=true" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const addresses = [
    'virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890',
    'virtual:f9e8d7c6-b5a4-3210-fedc-ba0987654321'
  ].join(',');

  const response = await fetch(
    `https://api.octav.fi/v1/virtual-users/portfolio?addresses=${addresses}&aggregated=true`,
    {
      headers: {
        'Authorization': `Bearer ${apiKey}`
      }
    }
  );

  const portfolios = await response.json();
  console.log(`Net Worth: $${portfolios[0].networth}`);
  ```

  ```python Python theme={null}
  import requests

  addresses = ','.join([
      'virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890',
      'virtual:f9e8d7c6-b5a4-3210-fedc-ba0987654321'
  ])

  response = requests.get(
      'https://api.octav.fi/v1/virtual-users/portfolio',
      params={
          'addresses': addresses,
          'aggregated': True,
          'includeImages': True
      },
      headers={'Authorization': f'Bearer {api_key}'}
  )

  portfolios = response.json()
  print(f"Net Worth: ${portfolios[0]['networth']}")
  ```

  ```typescript TypeScript theme={null}
  interface Asset {
    balance: string;
    symbol: string;
    price: string;
    value: string;
    contractAddress?: string;
  }

  interface Protocol {
    key: string;
    name: string;
    value: string;
    assets: Asset[];
  }

  interface Portfolio {
    address: string;
    networth: string;
    cashBalance: string;
    lastUpdated: string;
    assetByProtocols: Record<string, Protocol>;
    chains: Record<string, any>;
  }

  const addresses = [
    'virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890',
    'virtual:f9e8d7c6-b5a4-3210-fedc-ba0987654321'
  ].join(',');

  const response = await fetch(
    `https://api.octav.fi/v1/virtual-users/portfolio?addresses=${addresses}&aggregated=true`,
    {
      headers: {
        'Authorization': `Bearer ${apiKey}`
      }
    }
  );

  const portfolios: Portfolio[] = await response.json();
  console.log(`Net Worth: $${portfolios[0].networth}`);
  ```
</CodeGroup>

### Example Response

<Accordion title="View Full Response" icon="code">
  ```json theme={null}
  [
    {
      "address": "virtual:a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "cashBalance": "0",
      "dailyIncome": "0",
      "dailyExpense": "0",
      "fees": "0.05",
      "feesFiat": "160.25",
      "lastUpdated": "1715173392020",
      "networth": "28450.30",
      "assetByProtocols": {
        "wallet": {
          "key": "wallet",
          "name": "Wallet",
          "value": "18200.00",
          "assets": [
            {
              "balance": "3.2",
              "symbol": "ETH",
              "price": "3200.50",
              "value": "10241.60",
              "contractAddress": "0x0000000000000000000000000000000000000000",
              "chain": "ethereum"
            },
            {
              "balance": "7958.40",
              "symbol": "USDC",
              "price": "1.00",
              "value": "7958.40",
              "contractAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
              "chain": "ethereum"
            }
          ]
        },
        "aave_v3": {
          "key": "aave_v3",
          "name": "Aave V3",
          "value": "10250.30",
          "assets": [
            {
              "balance": "10250.30",
              "symbol": "USDT",
              "price": "1.00",
              "value": "10250.30",
              "contractAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7",
              "chain": "ethereum"
            }
          ]
        }
      },
      "chains": {
        "ethereum": {
          "key": "ethereum",
          "name": "Ethereum",
          "value": "28450.30",
          "protocols": ["wallet", "aave_v3"]
        }
      }
    }
  ]
  ```
</Accordion>

***

## Data Freshness

The Virtual Users Portfolio endpoint uses the same caching strategy as [Portfolio](/api/endpoints/portfolio):

<AccordionGroup>
  <Accordion title="How Caching Works" icon="clock">
    **Cache Duration:** 1 minute

    **When data is less than 1 minute old:**

    * Cached data returned immediately
    * Response time: under 100ms

    **When data is more than 1 minute old:**

    * Cached data returned immediately
    * Background sync initiated for next request
    * Next request gets fresh data

    **With waitForSync=true:**

    * Waits for sync if data is stale
    * Returns data less than 1 minute old
    * Response time: Variable (1-10 seconds)
  </Accordion>

  <Accordion title="Best Practices" icon="lightbulb">
    **For most use cases:**

    * Use default `waitForSync=false`
    * Data fresher than 1 minute is sufficient
    * Fast response times

    **For real-time tracking:**

    * Set `waitForSync=true` when you need the absolute latest data
    * Accept longer response times
    * Consider rate limits
  </Accordion>
</AccordionGroup>

***

## Use Cases

<Tabs>
  <Tab title="List & Query" icon="list">
    Discover virtual users and fetch their portfolios in a single flow:

    ```javascript theme={null}
    // Step 1: List all virtual users
    const usersResponse = await fetch(
      'https://api.octav.fi/v1/virtual-users',
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );
    const virtualUsers = await usersResponse.json();

    // Step 2: Fetch aggregated portfolio for all virtual users
    const addresses = virtualUsers.map(u => u.address).join(',');
    const portfolioResponse = await fetch(
      `https://api.octav.fi/v1/virtual-users/portfolio?addresses=${addresses}&aggregated=true`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );
    const portfolio = await portfolioResponse.json();
    console.log(`Total Net Worth: $${portfolio[0].networth}`);
    ```
  </Tab>

  <Tab title="Per-User Breakdown" icon="users">
    Fetch individual portfolios for each virtual user:

    ```javascript theme={null}
    const addresses = virtualUsers.map(u => u.address).join(',');
    const response = await fetch(
      `https://api.octav.fi/v1/virtual-users/portfolio?addresses=${addresses}`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );

    const portfolios = await response.json();
    portfolios.forEach(portfolio => {
      const user = virtualUsers.find(u => u.address === portfolio.address);
      console.log(`${user.label}: $${portfolio.networth}`);
    });
    ```
  </Tab>

  <Tab title="Combined View" icon="layer-group">
    Combine virtual user and wallet portfolios for a full picture:

    ```javascript theme={null}
    // Fetch wallet portfolio
    const walletPortfolio = await fetch(
      `https://api.octav.fi/v1/portfolio?addresses=${walletAddress}`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    ).then(r => r.json());

    // Fetch virtual user portfolio
    const virtualPortfolio = await fetch(
      `https://api.octav.fi/v1/virtual-users/portfolio?addresses=${virtualAddress}&aggregated=true`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    ).then(r => r.json());

    const totalNetWorth =
      parseFloat(walletPortfolio[0]?.networth ?? '0') +
      parseFloat(virtualPortfolio[0]?.networth ?? '0');
    console.log(`Combined Net Worth: $${totalNetWorth.toFixed(2)}`);
    ```
  </Tab>
</Tabs>

***

## Error Responses

<AccordionGroup>
  <Accordion title="400 Bad Request" icon="circle-exclamation">
    Invalid parameters provided.

    ```json theme={null}
    {
      "error": "Bad Request",
      "message": "addresses parameter is required"
    }
    ```

    **Common causes:**

    * Missing `addresses` parameter (portfolio endpoint)
    * Invalid address format (must be `virtual:<id>`)
    * More than 10 addresses in single request
  </Accordion>

  <Accordion title="401 Unauthorized" icon="lock">
    Authentication failed.

    ```json theme={null}
    {
      "error": "Unauthorized",
      "message": "Invalid API key"
    }
    ```

    **Solution:** Check your API key in the Authorization header
  </Accordion>

  <Accordion title="403 Forbidden" icon="ban">
    Attempted to access virtual users that don't belong to the authenticated API user.

    ```json theme={null}
    {
      "error": "Forbidden",
      "message": "One or more virtual user addresses do not belong to this account"
    }
    ```

    **Solution:** Only use virtual user addresses returned by `GET /v1/virtual-users` for your API key
  </Accordion>

  <Accordion title="429 Too Many Requests" icon="gauge-high">
    Rate limit exceeded.

    ```json theme={null}
    {
      "error": "Rate limit exceeded",
      "message": "You have exceeded your rate limit",
      "retry_after": 60
    }
    ```

    **Solution:** Wait for the specified time or implement retry logic
  </Accordion>

  <Accordion title="402 Payment Required" icon="credit-card">
    Insufficient credits.

    ```json theme={null}
    {
      "error": "Insufficient credits",
      "message": "Please purchase more credits to continue"
    }
    ```

    **Solution:** Purchase more credits at [data.octav.fi](https://data.octav.fi/)
  </Accordion>
</AccordionGroup>

***

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Portfolio" icon="wallet" href="/api/endpoints/portfolio">
    Retrieve portfolio holdings for wallet addresses
  </Card>

  <Card title="Historical Portfolio" icon="clock" href="/api/endpoints/historical-portfolio">
    View portfolio value at specific dates
  </Card>

  <Card title="Status" icon="signal" href="/api/endpoints/status">
    Check when portfolio was last synced
  </Card>

  <Card title="Credits" icon="coins" href="/api/endpoints/credits">
    Check your remaining API credits
  </Card>
</CardGroup>
