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

# Portfolio Wallets Quickstart

> Create a wallet, deposit funds, check balances, and withdraw.

This quickstart walks through listing available yield sources, creating a Portfolio Wallet, waiting for it to activate, funding it via its deposit addresses, checking balances, and withdrawing.

## Prerequisites

Set your base URL and API token once, then reuse them for every request.

Use sandbox while you build, then switch to production by swapping the base URL. The endpoint structure stays the same, but sandbox uses explicit testnet chain keys such as `ethereum_sepolia`.

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  export BASE_URL="https://sandbox.groundtech.co"
  export GROUND_API_KEY="your_api_token"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const BASE_URL = "https://sandbox.groundtech.co";
  const GROUND_API_KEY = "your_api_token";
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  BASE_URL = "https://sandbox.groundtech.co"
  GROUND_API_KEY = "your_api_token"
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  const BaseURL = "https://sandbox.groundtech.co"
  const GroundAPIKey = "your_api_token"
  ```
</CodeGroup>

## Conventions

* JSON field names are `camelCase`.
* Enum-like string values (for example `status`, `type`, and webhook `event`/`eventTypes`) are `lower_snake_case`.
  * Example: `payoutLeg.status = pending_customer_approval`

## 1. List available yield sources

Fetch the yield sources you can allocate to. Each yield source has a stable `id` you will use as `yieldSourceId` when creating a wallet.

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X GET "$BASE_URL/v2/wallets/yield-sources" \
    -H "Authorization: Bearer $GROUND_API_KEY"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(`${BASE_URL}/v2/wallets/yield-sources`, {
    headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
  });

  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.get(
      f"{BASE_URL}/v2/wallets/yield-sources",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
  )

  print(res.json())
  ```
</CodeGroup>

The response contains the catalog in its `data` array. Note each source's `id` and `apyBps` to decide your allocation.

## 2. Create a portfolio wallet

Create a Portfolio Wallet by passing `strategy.allocations` with your chosen yield allocation. Allocation percentages must sum to 100.

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "$BASE_URL/v2/wallets" \
    -H "Authorization: Bearer $GROUND_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "requestId": "123e4567-e89b-42d3-a456-426614174000",
      "label": "Core Yield Portfolio",
      "strategy": {
        "allocations": [
          { "yieldSourceId": "syrup-usdc", "pct": 35 },
          { "yieldSourceId": "morpho-gauntlet-usdc", "pct": 35 },
          { "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
        ]
      }
    }'
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(`${BASE_URL}/v2/wallets`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${GROUND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      requestId: "123e4567-e89b-42d3-a456-426614174000",
      label: "Core Yield Portfolio",
      strategy: {
        allocations: [
          { yieldSourceId: "syrup-usdc", pct: 35 },
          { yieldSourceId: "morpho-gauntlet-usdc", pct: 35 },
          { yieldSourceId: "morpho-steakhouse-usdc", pct: 30 },
        ],
      },
    }),
  });

  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.post(
      f"{BASE_URL}/v2/wallets",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
      json={
          "requestId": "123e4567-e89b-42d3-a456-426614174000",
          "label": "Core Yield Portfolio",
          "strategy": {
              "allocations": [
                  {"yieldSourceId": "syrup-usdc", "pct": 35},
                  {"yieldSourceId": "morpho-gauntlet-usdc", "pct": 35},
                  {"yieldSourceId": "morpho-steakhouse-usdc", "pct": 30},
              ],
          },
      },
  )

  print(res.json())
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	baseURL := os.Getenv("BASE_URL")
  	apiToken := os.Getenv("GROUND_API_KEY")

  	payload := map[string]any{
  		"requestId": "123e4567-e89b-42d3-a456-426614174000",
  		"label":     "Core Yield Portfolio",
  		"strategy": map[string]any{
  			"allocations": []map[string]any{
  				{"yieldSourceId": "syrup-usdc", "pct": 35},
  				{"yieldSourceId": "morpho-gauntlet-usdc", "pct": 35},
  				{"yieldSourceId": "morpho-steakhouse-usdc", "pct": 30},
  			},
  		},
  	}

  	bodyBytes, _ := json.Marshal(payload)
  	req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v2/wallets", baseURL), bytes.NewReader(bodyBytes))
  	req.Header.Set("Authorization", "Bearer "+apiToken)
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	body, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(body))
  }
  ```
</CodeGroup>

The v2 request shape uses token-keyed allocation groups with `yieldSourceId` and `pct`.

Wallet creation is asynchronous. The POST response returns immediately with `status: "creating"` and the wallet `id`, but `depositAddresses` are not yet available — they are populated once provisioning completes. Save the wallet id for the remaining steps:

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  export WALLET_ID="<wallet_id_from_create_response>"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const WALLET_ID = "<wallet_id_from_create_response>";
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  WALLET_ID = "<wallet_id_from_create_response>"
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  const WalletID = "<wallet_id_from_create_response>"
  ```
</CodeGroup>

## 3. Wait for the wallet to activate

Poll `GET /v2/wallets/{id}` every 1-2 seconds until `status === "idle"`. Only then are `depositAddresses` populated. If `status === "failed"`, inspect `failureReason` and retry creation with a new `requestId`.

See [Polling → Poll until wallet is active](/docs/portfolio-wallets/polling#poll-until-wallet-is-active) for a ready-to-paste snippet in cURL, Node, and Python.

## 4. Deposit actual funds

Once the wallet is `active`, grab the chain-specific deposit address from the poll response and send a stablecoin transfer from your custody to that address.

<CodeGroup dropdown>
  ```text Plain theme={"theme":{"light":"github-light","dark":"github-dark"}}
  to:    <depositAddresses.arbitrum>   (the 0x address from the GET wallet response after status becomes idle)
  chain: arbitrum
  token: usdc
  amount: 50,000.00
  ```
</CodeGroup>

## 5. Await Deposit Confirmation

Deposits are detected on-chain and then processed. You can track the latest deposit status either by polling the deposits endpoints or by subscribing to webhooks.

Poll (REST):

* List deposits for a wallet: `GET /v2/wallets/{id}/deposits`
* Fetch a single deposit: `GET /v2/wallets/{id}/deposits/{depositId}`

Example:

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/deposits?limit=25" \
    -H "Authorization: Bearer $GROUND_API_KEY"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(`${BASE_URL}/v2/wallets/${WALLET_ID}/deposits?limit=25`, {
    headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
  });

  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.get(
      f"{BASE_URL}/v2/wallets/{WALLET_ID}/deposits?limit=25",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
  )

  print(res.json())
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  import (
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	baseURL := os.Getenv("BASE_URL")
  	apiToken := os.Getenv("GROUND_API_KEY")
  	walletID := os.Getenv("WALLET_ID")

  	req, _ := http.NewRequest(
  		"GET",
  		fmt.Sprintf("%s/v2/wallets/%s/deposits?limit=25", baseURL, walletID),
  		nil,
  	)
  	req.Header.Set("Authorization", "Bearer "+apiToken)

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	body, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(body))
  }
  ```
</CodeGroup>

Webhook events:

* `portfolio_wallet.deposit.status_changed`

Possible deposit statuses (`deposit.status`):

* `processing`
* `completed`
* `failed`

## 6. Fetch the updated balance

Fetch the wallet to see current balances after the deposit is processed.

Key fields in the response:

* `balance.totalUsd` — total wallet value across positions, cash, and accrued yield
* `balance.withdrawableUsd` — conservative amount the customer can withdraw now
* `balance.reservedUsd` — customer-owned value currently reserved by active withdrawals or rebalances
* `balance.earnedUsd` — lifetime yield earned since wallet creation
* `positions[]` — current cash, bridge, and yield-source balances. Yield-source positions include target allocations.

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID" \
    -H "Authorization: Bearer $GROUND_API_KEY"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(`${BASE_URL}/v2/wallets/${WALLET_ID}`, {
    headers: { Authorization: `Bearer ${GROUND_API_KEY}` },
  });
  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.get(
      f"{BASE_URL}/v2/wallets/{WALLET_ID}",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
  )
  print(res.json())
  ```
</CodeGroup>

## 7. Withdraw (including the signing flow)

<Info>
  Ground uses Turnkey to manage signing flows, but you do not need a relationship with Turnkey to sign approvals.
</Info>

Initiate a withdrawal. If approval is required, a payout leg and its external payout step enter `pending_customer_approval`; complete the approval in Ground Portal or through the Ground Turnkey approval endpoints.

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals" \
    -H "Authorization: Bearer $GROUND_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
      "destinationChain": "ethereum_sepolia",
      "amountUsd": 65000,
      "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
    }'
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(`${BASE_URL}/v2/wallets/${WALLET_ID}/withdrawals`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${GROUND_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      requestId: "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
      destinationChain: "ethereum_sepolia",
      amountUsd: 65000,
      destinationAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    }),
  });

  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.post(
      f"{BASE_URL}/v2/wallets/{WALLET_ID}/withdrawals",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
      json={
          "requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
          "destinationChain": "ethereum_sepolia",
          "amountUsd": 65000,
          "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
      },
  )

  print(res.json())
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  import (
  	"bytes"
  	"encoding/json"
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	baseURL := os.Getenv("BASE_URL")
  	apiToken := os.Getenv("GROUND_API_KEY")
  	walletID := os.Getenv("WALLET_ID")

  	payload := map[string]any{
  		"requestId":          "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
  		"destinationChain":   "ethereum_sepolia",
  		"amountUsd":          65000,
  		"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  	}

  	bodyBytes, _ := json.Marshal(payload)
  	req, _ := http.NewRequest("POST", fmt.Sprintf("%s/v2/wallets/%s/withdrawals", baseURL, walletID), bytes.NewReader(bodyBytes))
  	req.Header.Set("Authorization", "Bearer "+apiToken)
  	req.Header.Set("Content-Type", "application/json")

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	body, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(body))
  }
  ```
</CodeGroup>

Save the `id` from the response (this is the withdrawal id used for status checks):

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  export WITHDRAWAL_ID="<withdrawal_id_from_withdraw_response>"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const WITHDRAWAL_ID = "<withdrawal_id_from_withdraw_response>";
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  WITHDRAWAL_ID = "<withdrawal_id_from_withdraw_response>"
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  const WithdrawalID = "<withdrawal_id_from_withdraw_response>"
  ```
</CodeGroup>

Customer approvals are completed in Ground Portal or through the Ground Turnkey approval endpoints: fetch `GET /v2/turnkey/activities/pending`, request `POST /v2/turnkey/activity-approval-request`, stamp the returned payload locally, then submit `POST /v2/turnkey/activities/{activityId}/vote`. See [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) for verification checks and approval patterns.

## 8. Await Withdrawal Confirmation

After approval (if required), the withdrawal is kicked off automatically. You can track the latest withdrawal status either by polling the withdrawal endpoint or by subscribing to webhooks.

Poll (REST):

<CodeGroup dropdown>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID" \
    -H "Authorization: Bearer $GROUND_API_KEY"
  ```

  ```javascript Node theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const res = await fetch(
    `${BASE_URL}/v2/wallets/${WALLET_ID}/withdrawals/${WITHDRAWAL_ID}`,
    { headers: { Authorization: `Bearer ${GROUND_API_KEY}` } },
  );

  console.log(await res.json());
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests

  res = requests.get(
      f"{BASE_URL}/v2/wallets/{WALLET_ID}/withdrawals/{WITHDRAWAL_ID}",
      headers={"Authorization": f"Bearer {GROUND_API_KEY}"},
  )

  print(res.json())
  ```

  ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}}
  package main

  import (
  	"fmt"
  	"io"
  	"net/http"
  	"os"
  )

  func main() {
  	baseURL := os.Getenv("BASE_URL")
  	apiToken := os.Getenv("GROUND_API_KEY")
  	walletID := os.Getenv("WALLET_ID")
  	withdrawalID := os.Getenv("WITHDRAWAL_ID")

  	req, _ := http.NewRequest(
  		"GET",
  		fmt.Sprintf("%s/v2/wallets/%s/withdrawals/%s", baseURL, walletID, withdrawalID),
  		nil,
  	)
  	req.Header.Set("Authorization", "Bearer "+apiToken)

  	resp, err := http.DefaultClient.Do(req)
  	if err != nil {
  		panic(err)
  	}
  	defer resp.Body.Close()

  	body, _ := io.ReadAll(resp.Body)
  	fmt.Println(string(body))
  }
  ```
</CodeGroup>

Webhook events:

* `portfolio_wallet.rebalance.status_changed` (cash deployment and strategy adjustment progress)
* `portfolio_wallet.withdrawal.status_changed`
* `portfolio_wallet.withdrawal.payout.status_changed` (per-leg payouts)

Possible withdrawal statuses (`withdrawal.status`):

* `processing`
* `partially_completed`
* `completed`
* `failed`
* `cancelled`

Possible payout leg statuses (`withdrawal.payoutLegs[].status`) and payout step states (`withdrawal.payoutLegs[].steps[].state`):

* `processing`
* `created`
* `pending_customer_approval`
* `completed`
* `failed`
* `cancelled`

Webhook payout and rebalance payloads include per-step workflow metadata such as `name`, `chain`, `state`, `txKind`, `stepKind`, `protocolType`, and `sequenceRole`. `txHash` is included when a broadcast transaction hash is available.

For more detail, see [Transaction Approvals](/docs/portfolio-wallets/transaction-approvals) and the API Reference withdrawal endpoints.
