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 without changing request shapes.
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:
withdrawal.status = pending_customer_approval
1. List available yield sources
Fetch the yield sources you can allocate to. Each yield source has a positionKey you’ll use when creating a wallet.
curl -X GET "$BASE_URL/v2/wallets/yield-sources" \
-H "Authorization: Bearer $GROUND_API_KEY"
The response contains a yieldSources array. Note the positionKey and currentRateBps for each source to decide your allocation.
2. Create a portfolio wallet
Create a Portfolio Wallet by passing positions with your chosen yield allocation. Positions must sum to 10,000 bps (100%).
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": "Treasury Portfolio",
"positions": [
{ "positionKey": "resolv_rlp", "targetWeightBps": 4000 },
{ "positionKey": "tbills", "targetWeightBps": 6000 }
]
}'
The response includes the wallet id and depositAddresses per chain. Save the wallet id for the remaining steps:
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.
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 -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
Per-leg status detail is available in deposit.legStatuses (keys vary by strategy/position). Possible leg statuses:
pending
processing
completed
skipped
failed
5. Fetch the updated balance
Fetch the wallet to see current balances after the deposit is processed.
Key fields in the response:
totalBalanceUsd — net assets (gross minus pending withdrawal reservations)
grossAssetsUsd — total value across all positions and cash
breakdownUsd — breakdown into cashUsd, inTransitUsd, positionClaimsUsd
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. The response includes a turnkeyActivityId if approval is required.
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",
"chain": "ethereum",
"token": "usdc",
"withdrawalAmount": 65000,
"destinationAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
}'
Save the id from the response (this is the withdrawal id used for status checks):
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.
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:
// const { id: withdrawalId, turnkeyActivityId } = withdrawalResponse;
// if (turnkeyActivityId) await approveTurnkeyActivity(turnkeyActivityId);
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 -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_liquidity
processing
pending_customer_approval
pending_broadcast
broadcasted
completed
partially_completed
failed
cancelled
For more detail, see Turnkey Approvals and the API Reference withdrawal endpoints.