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

# SOL Staking

> Stake, unstake and withdraw SOL through the Digital Asset Service Desk API, and track your stake accounts with the SOL staking dashboard.

Zodia Custody enables you to delegate SOL from a DeFi custody wallet to a Solana validator directly through the Digital Asset Service Desk API, and to track your stake accounts and balances with a dashboard endpoint. For ETH staking, see [ETH Staking](https://docs.zodia-custody.com/staking-guide.md).

## How SOL Staking Works

SOL staking is made of three separate operations, each submitted as its own service request:

| Operation | Service ID | What it does |
| - | - | - |
| Stake | `0x0020-024` | Delegates SOL to a validator. The stake account is `activating` until the delegation takes effect on the Solana network, then `active` and earning rewards. |
| Unstake | `0x0020-025` | Deactivates staked SOL. The stake account is `deactivating` until the end of the current Solana epoch (typically 2-3 days), then `inactive`. |
| Withdrawal | `0x0020-026` | Moves SOL from `inactive` stake accounts back to your wallet. |

Unstaking and withdrawing are always two steps, for every provider: unstake, wait for the cooldown to complete, then withdraw.

## Providers

Staking providers are onboarded for your company by Zodia, together with the validator your SOL is delegated to. You choose a provider in the `provider` field of the Stake payload, in upper case, for example `BLOCKDAEMON` or `MARINADE`. You don't choose the validator in the request. The payload is the same for every provider.

`MARINADE` differs from the other providers when withdrawing: a Withdrawal doesn't use `amount`, and everything that is ready to withdraw is withdrawn. With any other provider, the `amount` you send is withdrawn.

## Staking Operations

Every SOL staking operation is submitted the same way as any other Digital Asset Service Desk product: create the instruction with `POST /v3/api/servicedesk/create` and a `{"serviceId": ..., "payload": ...}` body, then submit, confirm and approve it following the same steps described in [Signing the Payload](https://docs.zodia-custody.com/authentication/signing-payload.md) and demonstrated end-to-end in [Wallet Creation](https://docs.zodia-custody.com/service-desk-guide/wallet-creation.md). What follows is what's specific to each SOL operation; see [Products & Services](https://docs.zodia-custody.com/service-desk-guide/products-and-services.md) for the exact payload template of each one.

All three operations require a DeFi wallet, otherwise the request is rejected with `ER-259`. Amounts are expressed in SOL and sent as strings.

### Stake (`0x0020-024`)

Staking delegates `amount` SOL from `walletId` through the `provider` you choose.

* **Minimum amount:** `amount` must be at least 1 SOL, for every provider.
* **Balance:** the wallet must hold the staked amount plus the network fee and the rent-exempt reserve of the new stake account.
* **One provider per wallet:** a wallet is pinned to the first staking provider it was used with. Staking again with a different provider is rejected with `ER-3004`.
* **Validator:** your SOL is delegated to the validator Zodia has set up for your account and the provider you choose. It isn't chosen in the request.

### Unstake (`0x0020-025`)

Unstaking deactivates `amount` SOL and starts the cooldown. `provider` isn't needed, it's taken from the wallet's staking history.

* **Amount:** `amount` can't exceed the amount currently available to unstake. A larger amount is rejected with `ER-122`, and the message gives the limit in lamports (1 SOL = 1,000,000,000 lamports). If the available amount can't be checked at that moment, the request fails with a `500` and can be retried.
* **Stake accounts:** you can't choose which stake accounts are unstaked. The amount is applied across your stake accounts, and an account may be split so that exactly `amount` is deactivated.
* **Available balance:** to create the request, the wallet must hold a minimum available SOL balance on top of the network fee, currently 1 SOL in production, whatever the amount you are unstaking. Otherwise the request is rejected with `ER-214` or `ER-255`.

Once the cooldown is over, the SOL moves to `inactive` stake accounts and shows up as `withdrawable` in the [dashboard](#tracking-your-position).

### Withdrawal (`0x0020-026`)

Withdrawal moves SOL from `inactive` stake accounts back to your custody wallet.

* **Amount:** `amount` can't exceed the withdrawable amount. A larger amount is rejected with `ER-122` (limit in lamports). If the withdrawable amount can't be checked at that moment, the request fails with a `500` and can be retried.
* **Marinade:** `amount` is not used. All funds that are ready to withdraw are withdrawn, and the request is rejected with `ER-ASE-011` if there are none.

## Tracking Your Position

[Get SOL Staking Dashboard](https://docs.zodia-custody.com/api-reference/staking-v3/get-sol-staking-dashboard.md) returns the staking position of a SOL wallet: balances, and the stake accounts behind them grouped by state. It takes the wallet ID only:

```json theme={null}
{
  "walletId": "Wallet_ID"
}
```

Here is an example response for a wallet with one active stake account and one inactive stake account that is ready to be withdrawn:

```json theme={null}
{
  "currency": "SOL",
  "balances": {
    "pendingStake": {
      "amount": "0.00000000",
      "amountUnit": "0",
      "fiatValue": 0.00,
      "fiatCurrency": "USD"
    },
    "activeStake": {
      "amount": "10.00000000",
      "amountUnit": "10,000,000,000",
      "fiatValue": 1500.00,
      "fiatCurrency": "USD"
    },
    "deactivatingStake": {
      "amount": "0.00000000",
      "amountUnit": "0",
      "fiatValue": 0.00,
      "fiatCurrency": "USD"
    },
    "withdrawable": {
      "amount": "2.00000000",
      "amountUnit": "2,000,000,000",
      "fiatValue": 300.00,
      "fiatCurrency": "USD"
    }
  },
  "stakeAccounts": {
    "activating": {
      "count": 0,
      "balance": { "amount": "0.00000000", "amountUnit": "0", "fiatValue": 0.00, "fiatCurrency": "USD" },
      "accounts": []
    },
    "active": {
      "count": 1,
      "balance": { "amount": "10.00200000", "amountUnit": "10,002,000,000", "fiatValue": 1500.30, "fiatCurrency": "USD" },
      "accounts": ["Stake_Account_Address"]
    },
    "deactivating": {
      "count": 0,
      "balance": { "amount": "0.00000000", "amountUnit": "0", "fiatValue": 0.00, "fiatCurrency": "USD" },
      "accounts": []
    },
    "inactive": {
      "count": 1,
      "balance": { "amount": "2.00200000", "amountUnit": "2,002,000,000", "fiatValue": 300.30, "fiatCurrency": "USD" },
      "accounts": ["Stake_Account_Address"]
    }
  },
  "provider": "BLOCKDAEMON",
  "lastUpdated": "2026-10-13T09:15:00Z"
}
```

### Balances

Each entry in `balances` covers the stake accounts in one state, and excludes the rent-exempt reserve that every stake account holds.

| Field | Stake accounts covered | Meaning |
| - | - | - |
| `pendingStake` | `activating` | SOL waiting to start earning rewards |
| `activeStake` | `active` | SOL delegated and earning rewards |
| `deactivatingStake` | `deactivating` | SOL in the cooldown period following an unstake |
| `withdrawable` | `inactive` | SOL that can be withdrawn |

`stakeAccounts` lists, for each state, the number of stake accounts (`count`), their addresses (`accounts`) and their combined `balance`. That balance includes the rent-exempt reserve of each account, so it is slightly higher than the matching figure in `balances`. Inactive accounts with nothing left to withdraw are not listed.

The dashboard covers every stake account controlled by the wallet, whichever provider it was created with, and doesn't break the position down by validator.

### Amounts

* `amount` is a string in SOL with 8 decimals and comma thousands separators. Use `amountUnit`, the amount in lamports, when you need the exact value.
* `fiatValue` is a number with 2 decimals, expressed in `fiatCurrency`, the fiat currency configured for your account.
* `provider` is the provider the wallet has staked with. It is left out when the wallet has no staking history.
* A wallet that has never staked returns zero amounts and empty `accounts` lists.

### Data Freshness

Dashboard figures are not real time. `lastUpdated` shows when they were last updated, and a stake, unstake or withdrawal can take up to about 15 minutes to be reflected. If the position can't be retrieved at that moment, the endpoint returns a `503` (`ER-206`): retry later.

## Notifications

Each SOL operation results in at least one on-chain transaction, and an Unstake or a Withdrawal can result in several, for example when several stake accounts are involved. If you have [webhooks](https://docs.zodia-custody.com/webhooks.md) configured, you receive Transfer notifications as each transaction is posted on chain and confirmed. To follow your overall position, use the dashboard: SOL moves to `withdrawable` once the cooldown following an unstake is over.

## Errors

See [Staking Error Codes](https://docs.zodia-custody.com/working-with-apis/staking-error-codes.md) for the errors specific to SOL staking.

## FAQs

### How long does it take for staked SOL to start earning rewards?

A stake account is `activating` until the delegation takes effect on the Solana network, which happens at an epoch boundary. Use the dashboard to follow it.

### Why does my dashboard not show my last operation yet?

Dashboard figures are not real time and can take up to about 15 minutes to reflect an operation. Compare `lastUpdated` with the time of your operation.

### Can I choose which stake account is unstaked or withdrawn?

No. You send an amount, and the provider decides which stake accounts are used.

### Why did my Unstake or Withdrawal result in several transactions?

An Unstake or a Withdrawal can involve several stake accounts, which results in several on-chain transactions. They all belong to the same service request, and each is notified separately through your webhooks as it is posted on chain and confirmed. The dashboard reflects the combined result.

## Contact

If you're a Zodia client and want to learn more about this feature or enable it, contact our customer service team.

Email us at [customerservice@zodia.io](mailto:customerservice@zodia.io). We're here to answer questions, provide more details, or help activate the feature. Our team is ready to assist you with quick and effective support.
