Skip to main content
This quickstart walks through listing available yield sources, creating a Portfolio Wallet, funding it via the returned 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.
cURL
export BASE_URL="https://sandbox.groundtech.co"
export GROUND_API_KEY="your_api_token"

Conventions

  • JSON field names are camelCase.
  • Enum-like string values (for example status, type, and webhook event/eventTypes) are lower_snake_case.
    • Example: payout.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.
cURL
curl -X GET "$BASE_URL/v2/wallets/yield-sources" \
  -H "Authorization: Bearer $GROUND_API_KEY"
The response contains a yieldSources 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.
cURL
curl -X POST "$BASE_URL/v2/wallets" \
  -H "Authorization: Bearer $GROUND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "123e4567-e89b-12d3-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 }
      ]
    }
  }'
The canonical v2 request shape uses yieldSourceId and pct. Legacy positionKey / targetWeightBps payloads remain accepted for backwards compatibility, but new integrations should use yield source IDs. The response includes the wallet id and depositAddresses per chain. Save the wallet id for the remaining steps:
cURL
export WALLET_ID="<wallet_id_from_create_response>"

3. Deposit actual funds

Send a stablecoin transfer from your custody to the chain you want to fund.
Plain
to:    <depositAddresses.arbitrum>   (the 0x address from the create response)
chain: arbitrum
token: usdc
amount: 50,000.00

4. 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:
cURL
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/deposits?limit=25" \
  -H "Authorization: Bearer $GROUND_API_KEY"
Webhook events:
  • portfolio_wallet.deposit.status_changed
Possible deposit statuses (deposit.status):
  • processing
  • completed
  • failed

5. 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.availableToInitiateUsd — same value on wallet responses, kept for backwards compatibility
  • balance.pendingWithdrawalUsd — active withdrawal encumbrances
  • balance.earnedUsd — lifetime yield earned since wallet creation
  • positions[] — current per-source balances and target allocations
cURL
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID" \
  -H "Authorization: Bearer $GROUND_API_KEY"

6. Withdraw (including the signing flow)

Ground uses Turnkey to manage signing flows, but you do not need a relationship with Turnkey to sign approvals.
Initiate a withdrawal. If approval is required, a payout leg later exposes a turnkeyActivityId while it is in pending_customer_approval.
cURL
curl -X POST "$BASE_URL/v2/wallets/$WALLET_ID/withdraw" \
  -H "Authorization: Bearer $GROUND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "df8b7be6-e110-4f6d-9b2d-7c44a5b1f0b0",
    "destinationChain": "ethereum",
    "amountUsd": 65000,
    "destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
  }'
Save the id from the response (this is the withdrawal id used for status checks):
cURL
export WITHDRAWAL_ID="<withdrawal_id_from_withdraw_response>"
Customer-side approval example (Turnkey):
Signing keys are provisioned and managed in your Ground Portal account. Ensure you always store signing keys safely as lost or compromised keys could result in lost funds.
Node
import { Turnkey } from "@turnkey/sdk-server";

// Mock keypair for docs only. Replace with your real customer signer key.
const TURNKEY_API_PUBLIC_KEY = "04aa...mock_public_key...ff";
const TURNKEY_API_PRIVATE_KEY = "11bb...mock_private_key...ee";
const TURNKEY_SUB_ORGANIZATION_ID = "your_turnkey_sub_organization_id";

const turnkey = new Turnkey({
  apiBaseUrl: "https://api.turnkey.com",
  defaultOrganizationId: TURNKEY_SUB_ORGANIZATION_ID,
  apiPublicKey: TURNKEY_API_PUBLIC_KEY,
  apiPrivateKey: TURNKEY_API_PRIVATE_KEY,
});

const apiClient = turnkey.apiClient();

export async function approveTurnkeyActivity(turnkeyActivityId) {
  // 1) Fetch the activity to get its fingerprint
  const { activity } = await apiClient.getActivity({
    organizationId: TURNKEY_SUB_ORGANIZATION_ID,
    activityId: turnkeyActivityId,
  });

  const fingerprint = activity?.fingerprint;
  if (!fingerprint) {
    throw new Error("Turnkey activity fingerprint missing");
  }

  // 2) Approve it (this registers your approval vote in Turnkey)
  await apiClient.approveActivity({
    type: "ACTIVITY_TYPE_APPROVE_ACTIVITY",
    timestampMs: String(Date.now()),
    organizationId: TURNKEY_SUB_ORGANIZATION_ID,
    parameters: { fingerprint },
  });
}

// Usage:
// Load the withdrawal and inspect its payout legs.
// Approve any payout with status === "pending_customer_approval".

7. 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):
cURL
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals/$WITHDRAWAL_ID" \
  -H "Authorization: Bearer $GROUND_API_KEY"
Webhook events:
  • portfolio_wallet.withdrawal.status_changed
  • portfolio_wallet.withdrawal.payout.status_changed (per-leg payouts)
Possible withdrawal statuses (withdrawal.status):
  • pending
  • processing
  • completed
  • failed
  • cancelled
Possible payout statuses (withdrawal.payouts[].status):
  • planning
  • processing
  • pending_customer_approval
  • pending_broadcast
  • broadcasted
  • completed
  • failed
  • cancelled
  • skipped
For more detail, see Transaction Approvals and the API Reference withdrawal endpoints.