Staking
Introduction
zerohash offers comprehensive staking services that allow platforms to enable crypto staking for their users. This guide provides best practices for integration, explains the staking lifecycle, and offers guidance on building a seamless user experience.
The designs referenced in this documentation represent our conceptual SDK framework. For access to the complete design specifications and additional technical resources, please contact our integration team.
Please note this guide reflects our current beta implementation and is subject to change. As we continue enhancing our staking infrastructure, specifications may evolve to incorporate user feedback and platform improvements.
Prerequisites
Before enabling staking functionality, ensure your integration supports these requirements:
- KYC verification: Users must complete identity verification
- Jurisdiction compliance: Staking must be enabled for user's jurisdiction
- Terms acceptance: Users must accept staking terms and conditions
Core Concepts
Definitions
| Term | Definition |
|---|---|
| Activation Queue | New validators must wait in a queue before becoming active. Queue length affects how long it takes for newly staked ETH to start earning rewards (typically 1-4 weeks). |
| Activation Time | The delay between submitting a stake transaction and when the stake becomes active and starts earning rewards. Includes blockchain confirmation time plus any network-specific activation queues. |
| Unstake Period | The mandatory waiting period between initiating an unstake request and funds becoming available for withdrawal. This period varies by network. |
Supported Assets
Currently supported cryptocurrencies with staking:
- Ethereum (ETH)
- Solana (SOL) - Late Q1 2026
- Additional assets: Coming soon!
Staking Lifecycle and State Machine
The staking process follows a clear six-step journey from discovery to unstaking. We recommend using both API polling and webhook notifications to track a stake's status throughout this flow.
State Flow
- Submitted → Request accepted, queued for blockchain broadcast
- Broadcasted → Transaction sent to network
- Confirmed → On-chain confirmation received
- Staked → Terminal state; actively earning rewards
- Unstaked → Terminal state; no longer earning rewards or unstaking
- Canceled → Terminal state; user canceled before broadcast
- Failed → Terminal state; transaction rejected (requires new submission)
User Flow
1. Discovery and Initiation
Help users discover which assets they can stake and understand the terms.
- Use GET /assets?staking_enabled=true to retrieve only stakeable assets
- Display staking information using GET /assets/{asset}/staking_info
- ⚠️ Please note all the staking information provided at
GET /assetsare estimates.
- ⚠️ Please note all the staking information provided at
- Show available balances via GET /accounts?account_type=available
UX Best Practices
- Make the unstaking period impossible to miss - confusion on this can lead to high volumes of support issues
- Highlight that APY estimates may fluctuate based on network conditions
- Consider showing a simple ROI calculator for different stake amounts
- For restricted jurisdictions, suppress all staking UI entirely
2. Eligibility and Compliance
Before allowing stake submission, confirm the user has accepted terms and has sufficient balance.
- Check terms acceptance status via GET /participant/{participant_code}/full_info
- Check for the
"signed_agreements"array element with"type" : "staking"
- Check for the
- If terms not accepted, prompt user and update via PATCH /participants/customers/{participant_code}
- Validate balance using GET /accounts?account_type=available before showing stake confirmation screen
- Store terms acceptance status locally to minimize API calls
⚠️ Important: All staking operations are blocked until terms are accepted. Single acceptance enables staking for ALL supported assets.
UX Best Practices
- Present terms acceptance as a one-time action that enables staking for all assets
- Explain this is required for regulatory compliance
- For users in restricted jurisdictions, do not surface any staking UX
3. Staking Execution
Once the user confirms their stake details, submit the request and begin tracking.
- User confirms staking transaction details (amount, estimated reward APY, estimated unstaking period)
- Submit stakes using POST /stakes
- System validates request and returns stake ID for tracking - store the returned
stake_id- you'll need it for future operations - Listen for
stake.submittedwebhook - Stake requests can be canceled before they have been broadcasted via POST /stakes/{stake_id}/cancel
⚠️ Important: Once broadcasted to the blockchain, cancellation is not possible. Users must wait for activation to complete, then unstake.
UX Best Practices
- Show a clear confirmation screen with all terms before submission
- Provide immediate visual feedback that submission succeeded
- Make the cancel option obvious while it's still available and hide when unavailable
4. Activation and Monitoring
Monitor stake progress as it moves from submission to actively earning rewards.
- Stakes move through several states, and real-time statuses will be provided via webhooks:
stake.submitted→ Request accepted, queued for processingstake.canceled→ User canceled a stake request before it was broadcastedstake.broadcasted→ Transaction sent to blockchainstake.confirmed→ Transaction confirmed on-chainstake.staked→ Actively earning rewardsstake.failed→ Stake request failed and must be requested again
- Monitor stake status via GET /stakes/{stake_id}/status
UX Best Practices
- Show progress indicators during activation period
- Explain that activation time varies by network conditions (1-4 weeks typical for ETH)
- Notify users when their stake becomes active and starts earning
- For failed stakes, clearly explain that the user needs to resubmit
5. Ongoing Management
Once active, rewards accrue daily and are automatically distributed to the user's balance.
- Subscribe to
staking_reward.receivedwebhooks for real-time reward notifications - Use GET /stakes/{participant_code} for portfolio overview
- Use GET /stakes/{participant_code}/rewards for detailed reward history
- Refresh portfolio data periodically (e.g., every 5 minutes when user is viewing)
UX Best Practices
- Display total staked value, total rewards earned, and current estimated APY
- Include rewards as distinct line items in transaction history sections
- Allow users to view individual stakes or consolidated portfolio view
6. Unstaking
When users want to access their staked funds, guide them through the unstaking process and waiting period.
- Check staked balances via GET /accounts?account_type=staked
- Submit unstake requests using POST /stakes/unstake
- Store unstake_id for tracking and support purposes
- Unstakes move through several states, and real-time statuses will be provided via webhooks:
unstake.submitted→ Request accepted, queued for processingunstake.canceled→ User canceled an unstake request before it was broadcastedunstake.broadcasted→ Transaction sent to blockchainunstake.confirmed→ Transaction confirmed on-chainunstake.unstaked→ No longer staked, balance available to transact/tradeunstake.failed→ Unstake request failed and must be requested again
UX Best Practices
- Show unstaking period prominently before user commits
- Explain that rewards continue to accrue until unstake reaches terminal unstaked state
- Notify users when funds become available to trade or transact
Account Types and Ledger Balances
zerohash uses three distinct account types to track staking lifecycle states. Understanding how balances move between these accounts is essential for accurate balance reporting and transaction handling.
Account Type Definitions
account_type | Description | Use Cases |
|---|---|---|
available | Funds that can be used to transact, trade, or stake | Buying, selling, transferring, withdrawing, or initiating new stakes |
collateral | Funds pending activation or pending unstaking period | Monitoring stakes during activation queue or unstaking waiting period |
staked | Funds actively staked and earning rewards | Tracking active staking positions and accrued rewards |
Balance Flow Rules
During Staking:
- Funds move from
available→collateralwhen a stake is submitted - Funds move from
collateral→stakedwhen the stake is confirmed and actively earning rewards
During Unstaking:
- Funds move from
staked→collateralwhen an unstake is submitted - Funds move from
collateral→availablewhen the unstaking period completes
Important Constraints
⚠️ Critical Balance Rules:
- Balances in
collateralaccount_typemust reach the staked state before they can be unstaked - Balances in
stakedaccount_typemust be unstaked before participants can withdraw or sell those assets - Only balances in the
availableaccount_typecan be used for trading, transfers, or withdrawals
💡 Implementation Tip: Use GET /accounts?account_type={type} to query specific account types when displaying balances to users. Show all three account types in staking portfolio views to provide complete transparency.
Webhooks
Webhook Event Configuration
Set up webhook endpoints to receive real-time notifications for staking events. Configure your webhook URL and specify which events to receive.
{
"webhook_url": "<https://yourapi.com/webhooks/staking">,
"events": [
"stake.submitted",
"stake.canceled",
"stake.broadcasted",
"stake.confirmed",
"stake.staked",
"stake.failed",
"staking_reward.received",
"unstake.submitted",
"unstake.canceled",
"unstake.broadcasted",
"unstake.confirmed",
"unstake.unstaked",
"unstake.failed"
]
}
Webhook Event Reference
stake.submitted
Sent immediately after stake request is accepted and queued for processing.
{
"event_type": "stake.submitted",
"event_id": "evt_123456789",
"timestamp": "2025-08-24T16:00:00.123Z",
"participant_code": "CUST01",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"notional": "35000.00",
"notional_asset": "usd",
"status": "submitted"
}
stake.confirmed
Sent when the staking transaction is confirmed on the blockchain and begins earning rewards.
{
"event_type": "stake.confirmed",
"event_id": "evt_234567890",
"timestamp": "2025-08-24T16:15:00.123Z",
"participant_code": "CUST01",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"status": "confirmed"
}
staking_reward.received
Sent when staking rewards are distributed and credited to the user's account.
{
"event_type": "staking_reward.received",
"event_id": "evt_345678901",
"timestamp": "2025-08-25T12:00:00.123Z",
"participant_code": "CUST01",
"reward_amount": "0.025",
"asset": "ETH",
"apy": "3.85",
"reward_date": "2025-08-25",
"notional": "83.25",
"notional_asset": "usd",
"cumulative_rewards_amount": "0.125",
"cumulative_rewards_notional": "100.00"
}
unstake.submitted
Sent when an unstake request is submitted by the user.
{
"event_type": "unstake.submitted",
"event_id": "evt_345678901",
"participant_code": "string",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"notional": "35000.00",
"notional_asset": "usd",
"status": "submitted"
}
unstake.confirmed
Sent when the staking transaction is confirmed on the blockchain and begins earning rewards.
{
"event_type": "unstake.confirmed",
"event_id": "evt_234567890",
"timestamp": "2025-08-24T16:15:00.123Z",
"participant_code": "CUST01",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"status": "confirmed"
}
Implementation Best Practices
- Acknowledge immediately: Always return 200 OK to acknowledge webhook receipt
- Process asynchronously: Handle webhook processing outside the request/response cycle to avoid timeouts
- Store payloads: Maintain webhook event logs for debugging and audit trails
- Handle duplicates: Implement idempotency using
event_idto prevent duplicate processing - Fallback strategy: Implement polling as a backup if webhook delivery fails
- Verify authenticity: Validate webhook signatures to ensure requests come from zerohash
- Monitor failures: Track webhook delivery failures and alert on persistent issues
Error Handling
Common validation errors and recommended handling:
- Terms not accepted: Redirect user to T&C acceptance flow
- Insufficient balance: Display current balance
- Jurisdiction restrictions: Display appropriate messaging about availability
- Asset not supported: Filter out non-stakeable assets in UI
Error Response Format
All error responses follow this structure:
{
"error_code": "ERROR_IDENTIFIER",
"message": "Technical error description",
"details": {
"field": "specific_field",
"value": "problematic_value",
"additional_context": {}
},
"user_message": "User-friendly error message",
"request_id": "req_123456789",
"timestamp": "2025-08-24T16:00:00Z"
}
Common Error Codes
Validation Errors (400)
TERMS_NOT_ACCEPTED
{
"error_code": "TERMS_NOT_ACCEPTED",
"message": "User must accept staking terms before proceeding",
"user_message": "Please review and accept our staking terms and conditions"
}
INSUFFICIENT_BALANCE
{
"error_code": "INSUFFICIENT_BALANCE",
"message": "Available balance insufficient for requested stake amount",
"details": {
"asset": "ETH",
"requested": "10.5",
"available": "8.2"
},
"user_message": "You need at least 10.5 ETH to complete this stake"
}
JURISDICTION_RESTRICTED
{
"error_code": "JURISDICTION_RESTRICTED",
"message": "Staking not available in user's jurisdiction",
"details": {
"participant_code": "CUST01",
"jurisdiction": "NY"
},
"user_message": "Staking is not currently available in your location"
}
Authentication Errors (401)
INVALID_TOKEN
{
"error_code": "INVALID_TOKEN",
"message": "Authentication token is invalid or expired",
"user_message": "Please log in again"
}
Server Errors (500)
INTERNAL_SERVER_ERROR
{
"error_code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected error occurred",
"user_message": "Something went wrong. Please try again later"
}
Quick Reference
Essential Endpoints
- Discover stakeable assets: GET /assets?staking_enabled=true
- Validate available balance: GET /accounts?account_type=available
- Check staking parameters: GET /assets/{asset}/staking_info
- Verify terms acceptance: GET /participant/{participant_code}/full_info
- Submit stake: POST /stakes
- Monitor status: GET /stakes/{stake_id}/status
- View portfolio: GET /stakes/{participant_code}
- Track rewards: GET /stakes/{participant_code}/rewards
- Unstake funds: POST /stakes/unstake
Support and Resources
For additional assistance:
- Integration support: Please contact your dedicated Relationship Manager
- Design resources: Request FE design specifications
Updated 1 day ago
