> ## 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.

# API Conventions

> Authentication, environments, pagination, idempotency, rate limiting, and error handling.

This page covers the conventions shared across all Portfolio Wallet API endpoints.

## Base path

Wallet resources use the `/v2/wallets` path prefix. Supporting APIs use their own `/v2/*` prefixes, including `/v2/activity`, `/v2/accounting`, `/v2/webhooks`, `/v2/address-book`, `/v2/turnkey`, and `/v2/system`.

## Authentication

All requests require a Bearer token in the `Authorization` header. If the token is missing or invalid, the API returns `401`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "https://sandbox.groundtech.co/v2/wallets" \
  -H "Authorization: Bearer $API_TOKEN"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "data": [
    {
      "id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
      "label": "Core Yield Portfolio",
      "status": "idle",
      "balance": {
        "totalUsd": "82500.000000",
        "earnedUsd": "250.000000",
        "reservedUsd": "0.000000",
        "withdrawableUsd": "82500.000000"
      },
      "createdAt": "2026-02-05T08:15:00.000Z",
      "positions": [
        { "id": "cash:ethereum_sepolia:usdc", "kind": "cash", "label": "Cash (Ethereum Sepolia)", "valueUsd": "0.000000" },
        { "id": "syrup-usdc", "pct": 40, "kind": "yield_source", "label": "Syrup USDC", "valueUsd": "33000.000000", "yieldSourceId": "syrup-usdc", "mode": "active", "apyBps": 491 },
        { "id": "morpho-gauntlet-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Gauntlet USDC Prime", "valueUsd": "24750.000000", "yieldSourceId": "morpho-gauntlet-usdc", "mode": "active", "apyBps": 500 },
        { "id": "morpho-steakhouse-usdc", "pct": 30, "kind": "yield_source", "label": "Morpho Steakhouse USDC Prime", "valueUsd": "24750.000000", "yieldSourceId": "morpho-steakhouse-usdc", "mode": "active", "apyBps": 500 }
      ],
      "failureReason": null,
      "depositAddresses": {
        "solana_devnet": "DmaePN7kL6WjSecMaH4urdS8zN8FwNPTxbdTqFwAw6Fd",
        "ethereum_sepolia": "0xFf71D07826d0aFcEcDF07Be9c499f9DDF3ee5cFa"
      }
    }
  ],
  "nextCursor": null
}
```

## Environments

| Environment | Base URL |
| - | - |
| Sandbox | `https://sandbox.groundtech.co` |
| Production | `https://production.groundtech.co` |

Sandbox focuses on test networks while you integrate. Production processes live funds on supported chains. Swap the base URL to move between environments, but note that sandbox uses explicit testnet chain keys such as `ethereum_sepolia` and may expose a smaller yield-source catalog than production.

Sandbox is limited to a subset of networks for integration testing. Unlike production, sandbox chain keys include the network suffix to make the testnet explicit:

* `ethereum_sepolia` -- Ethereum Sepolia testnet
* `solana_devnet` -- Solana devnet
* `tron_shasta` -- TRON Shasta testnet, when enabled

Production uses canonical chain names (`arbitrum`, `base`, `ethereum`, `polygon`, `solana`, and `tron` when enabled). See [Supported Chains](/docs/portfolio-wallets/supported-chains) for details.

## Pagination

List endpoints use **cursor-based** pagination with `limit` and `cursor` parameters.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25" \
  -H "Authorization: Bearer $API_TOKEN"
```

The response includes a `nextCursor` field. Pass it as `cursor` to fetch the next page:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $API_TOKEN"
```

When `nextCursor` is `null`, you have reached the end of the result set.

Endpoint-specific filters, sort options, limits, and status semantics vary by endpoint. Treat each API reference page's parameter table as authoritative for that endpoint.

## Idempotency

Create and withdrawal endpoints accept a `requestId` (UUID v4). These endpoints return **`200 OK`** for both the initial request and an idempotent replay with the same `requestId` — the existing resource state is returned on replay.

Wallet and withdrawal creation use `requestId` to find and return the existing resource on replay. Do not rely on these create endpoints to compare the replayed payload field-by-field.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/wallets" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "requestId": "f0a3afc3-1e67-4c0c-a8e1-3f5d8fb9a23b",
    "label": "Core Yield Portfolio",
    "strategy": {
      "allocations": {
        "usdc": [
          { "yieldSourceId": "syrup-usdc", "pct": 40 },
          { "yieldSourceId": "morpho-gauntlet-usdc", "pct": 30 },
          { "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
        ],
        "usdt": [
          { "yieldSourceId": "cash", "pct": 100 }
        ]
      }
    }
  }'
```

The public write shape uses token-keyed allocation groups with `yieldSourceId` and `pct`. Each included group independently totals 100%.

Allocation `pct` values support 0.1% increments and must sum to 100.

Replaying the same `requestId` with the **same** payload returns `200 OK` with the existing resource. Replaying a `requestId` with a **different** payload returns `409 request_id_conflict`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": "requestId has already been used with a different payload",
  "code": "request_id_conflict",
  "details": "The requestId 1f1c3b2a-0000-4000-8000-000000000001 was first used with a different allocation."
}
```

When you receive a `409`, fetch the existing resource by `requestId` rather than retrying the create.

## Rate limiting

Each API key can make up to **300 authenticated requests per minute** across the API. Every request counts toward this overall limit. Endpoints with a lower limit must also stay within their listed limit.

Rate limits use two limit types:

* **Per API key** — one allowance is shared by all calls to that endpoint using the API key.
* **Per API key, per wallet** — each wallet gets its own allowance under the API key. For example, you can update Wallet A five times and Wallet B five times in the same minute. Both wallets' calls still count toward the API key's overall 300-request limit.

| Endpoint | Requests allowed | Limit type |
| - | - | - |
| All authenticated `/v2/*` endpoints | 300 per minute | Per API key |
| `GET /v2/activity` | 10 per minute | Per API key |
| `GET /v2/activity?walletId={walletId}` | 10 per minute | Per API key, per wallet |
| `GET /v2/accounting/activity` | 10 per minute | Per API key |
| `GET /v2/accounting/yield/period-summary` | 10 per minute | Per API key |
| `GET /v2/wallets/overview` | 30 per minute | Per API key |
| `GET /v2/wallets/{id}` | 30 per minute | Per API key, per wallet |
| `GET /v2/wallets/{id}/yield` | 30 per minute | Per API key, per wallet |
| `POST /v2/wallets` | 5 per minute | Per API key |
| `PATCH /v2/wallets/{id}/strategy` | 5 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/allocate-preview` | 10 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/allocate` | 5 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/withdrawal-preview` | 10 per minute | Per API key, per wallet |
| `POST /v2/wallets/{id}/withdrawals` | 5 per minute | Per API key, per wallet |
| `POST /v2/webhooks` | 10 per minute | Per API key |
| `DELETE /v2/webhooks/{id}` | 10 per minute | Per API key |
| `POST /v2/address-book/entries` | 30 per minute | Per API key |
| `DELETE /v2/address-book/entries/{id}` | 30 per minute | Per API key |

If you exceed a limit, the API returns `429 Too Many Requests`. The `Retry-After` header tells you how many seconds to wait. Wait at least that long, add a small random delay, and then try again.

## Error handling

Business API errors are returned as JSON with a human-readable `error` string and a machine-readable `code` field.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": "Missing required fields: destinationAddress, amountUsd, destinationChain, requestId",
  "code": "validation_error"
}
```

| HTTP status | Meaning | Action |
| - | - | - |
| `400` | Validation error | Fix the request and retry |
| `401` | Authentication failed | Check your Bearer token |
| `404` | Resource not found | Verify the wallet/withdrawal ID |
| `409` | `requestId` reused with a different payload (`request_id_conflict`) | Fetch the existing resource instead |
| `429` | Rate limited | Wait for `Retry-After`, add a small random delay, and try again |
| `500` | Server error | Log the response and retry with backoff |

Business API errors include both `error` and `code` fields. Authentication middleware can return `{ "error": "...", "message": "..." }` before the request reaches a business handler. Use `code` for programmatic branching when it is present, and use the `error` string for logging and debugging.


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