Staking
Introduction
zerohash offers comprehensive staking API services that allow platforms to enable native 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 a conceptual UX framework. For access to the complete design specifications and additional technical resources, please contact your zerohash representative.
Staking is currently available via API only — there is no SDK integration path at this time. zerohash also does not offer liquid staking; staked positions are illiquid for the duration of the network's unstake period and cannot be represented by a transferable receipt token.
Prerequisites
Before enabling staking functionality, ensure your integration supports these requirements:
- KYC verification: End users must be fully KYC-approved zerohash participants. Staking is not available to participants in
pendingorrestrictedstatus. - Jurisdiction eligibility: The participant's resident jurisdiction must be on the staking-permitted list. At this time, staking is prohibited in the following five states: CA, MD, NJ, WI, and WA.
- Terms acceptance: Each participant must have an active
signed_agreementoftype: "staking"on file. A single acceptance enables staking across all supported assets for that participant. - Webhook endpoint: A configured, signature-verified webhook endpoint to receive staking lifecycle events (see Webhooks section below).
- Account funded: The participant must hold a sufficient
availablebalance of the asset being staked.
Core Concepts
Definitions
| Term | Definition |
|---|---|
| Activation Queue | New validators must wait in a network-defined queue before becoming active. Queue length affects how long it takes for newly staked funds to start earning rewards. |
| Activation Time | The delay between submitting a stake transaction and the stake becoming active. Includes blockchain confirmation plus any network-specific activation queue. |
| Est. APY (Net) | Annualized percentage yield, expressed net of your Platform fee charged. This is always an estimate - actual rewards vary with network conditions and validator performance. |
| Unstake Period | The mandatory waiting period between initiating an unstake and the funds returning to the available account. Unstake periods are network-specific. Users will continue to earn rewards during this period until the unstaking period is complete. |
| Validator | The node that produces blocks and attests to network state in exchange for rewards. zerohash manages this infrastructure on behalf of platforms. |
| Consensus Rewards | The base protocol-level rewards paid to validators for attestation and block proposal. |
| MEV Rewards | Additional rewards from Maximal Extractable Value capture (priority fees, block ordering). Reported separately from consensus rewards. |
Supported Assets
Currently supported cryptocurrencies with staking:
-
ETH (Ethereum)
-
SOL (Solana) - Q3 2026
Additional assets: Coming soon!
Configurable Platform Fees
Staking yield is reported net of the platform fee you choose to set. The advertised APY in GET /assets/{asset}/staking_info already reflects this deduction.
Staking on zerohash uses a fee model based on gross network rewards. Platforms set their own end-user pricing.
How the model works
- Gross rewards are paid by the network to the staking position.
- Your platform applies a platform fee — a percentage deducted from gross rewards before the remainder is credited to the user. zerohash recommends a platform fee of 25–35% (i.e. users receive 65–75% of gross rewards), but platforms control their own pricing and can configure higher or lower.
- The user's net reward equals gross rewards minus the platform fee.
- The platform earns revenue from the platform fee, net of zerohash's processing fee. Specific commercial terms are set in your zerohash agreement — contact your Relationship Manager for more details on this.
| Component | Ex. Amount | % of gross rewards |
|---|---|---|
| Gross rewards | $100 | 100% |
| User reward (pass-through) | $70 | 70% |
| Platform fee | $30 | 30% |
ℹ️ Note on apy_net: The APY returned by GET /assets/{asset}{asset}/staking_info is platform-specific and reflects your configured platform fee. Different platforms querying the same asset will see different apy_net values.
Staking Lifecycle and State Machine
The staking process follows a clear journey from discovery to unstaking. We recommend using both API polling and webhook notifications to track a stake's status throughout this flow.
Stake States
submitted— Request accepted; queued for blockchain broadcast.broadcasted— Transaction signed and sent to the network.confirmed— Stake entered the consensus activation queue. (Note: on-chain broadcast acknowledgement is signaled bybroadcasted.)staked— Terminal active state; position is earning rewards.canceled— Terminal; user canceled before broadcast.failed— Terminal; transaction rejected. Requires resubmission.
Unstake States
submitted— Unstake request accepted; queued for blockchain broadcast.broadcasted— Unstake transaction sent to network.confirmed— Unstake entered the exit queue (cooling-down); unstaking waiting period underway.unstaked— Terminal; funds returned to the available account.canceled— Terminal; user canceled before broadcast.failed— Terminal; unstake transaction rejected. Requires resubmission.

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 and subject to change with network conditions.
- ⚠️ 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
Track 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.
Stake and Unstake Minimums
No minimums. Users can stake or unstake any amount they hold in available or staked respectively.
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
stake.submittedSent immediately after stake request is accepted and queued for processing.
{
"event_type": "stake.submitted",
"timestamp": "2026-05-21T16:00:00.123Z",
"occurred_at": "2026-05-21T16:00:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "submitted"
}stake.canceled
stake.canceledSent immediately after stake request is canceled.
{
"event_type": "stake.canceled",
"timestamp": "2026-05-21T16:02:00.123Z",
"occurred_at": "2026-05-21T16:02:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "canceled",
"reason": "user_initiated"
}stake.broadcasted
stake.broadcastedSent when the stake transaction is signed and broadcast to the network.
{
"event_type": "stake.broadcasted",
"timestamp": "2026-05-21T16:05:00.123Z",
"occurred_at": "2026-05-21T16:05:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "broadcasted"
}stake.confirmed
stake.confirmedSent when the stake enters the consensus activation queue and pool allocation begins. On-chain broadcast confirmation is stake.broadcasted.
{
"event_type": "stake.confirmed",
"timestamp": "2026-05-21T16:15:00.123Z",
"occurred_at": "2026-05-21T16:15:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "confirmed",
"tx_hash": "0xabc..."
}stake.staked
stake.stakedSent when the stake completes activation and begins earning rewards.
{
"event_type": "stake.staked",
"timestamp": "2026-05-28T12:00:00.123Z",
"occurred_at": "2026-05-28T12:00:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "staked",
"tx_hash": "0xabc..."
}stake.failed
stake.failedSent when the stake request fails. Includes a reason field for diagnostics.
{
"event_type": "stake.failed",
"timestamp": "2026-05-21T16:10:00.123Z",
"occurred_at": "2026-05-21T16:10:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"amount": "10.5",
"status": "failed",
"reason": "pool_capacity_exceeded"
}staking_reward.received
staking_reward.receivedSent when staking rewards are distributed and credited to the participant's account.
{
"event_type": "staking_reward.received",
"timestamp": "2026-05-22T12:00:00.123Z",
"credited_at": "2026-05-22T12:00:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"distribution_id": "dist_abc123",
"asset": "ETH",
"status": "credited",
"reward_amount": "0.025",
"cumulative_rewards_amount": "0.125"
}unstake.submitted
unstake.submitted{
"event_type": "unstake.submitted",
"timestamp": "2026-05-30T10:00:00.123Z",
"occurred_at": "2026-05-30T10:00:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "submitted"
}unstake.canceled
unstake.canceled{
"event_type": "unstake.canceled",
"timestamp": "2026-05-30T10:01:00.123Z",
"occurred_at": "2026-05-30T10:01:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "canceled",
"reason": "user_initiated"
}unstake.broadcasted
unstake.broadcasted{
"event_type": "unstake.broadcasted",
"timestamp": "2026-05-30T10:05:00.123Z",
"occurred_at": "2026-05-30T10:05:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "broadcasted"
}unstake.confirmed
unstake.confirmedSent when the unstake enters the exit queue (cooling-down).
{
"event_type": "unstake.confirmed",
"timestamp": "2026-05-30T10:15:00.123Z",
"occurred_at": "2026-05-30T10:15:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "confirmed",
"tx_hash": "0xdef..."
}unstake.unstaked
unstake.unstakedSent when unstake period completes and funds are returned to the available account.
{
"event_type": "unstake.unstaked",
"timestamp": "2026-06-03T08:00:00.123Z",
"occurred_at": "2026-06-03T08:00:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "unstaked"
}unstake.failed
unstake.failedSent when unstake period completes and funds are returned to the available account.
{
"event_type": "unstake.failed",
"timestamp": "2026-05-30T10:10:00.123Z",
"occurred_at": "2026-05-30T10:10:00.123Z",
"participant_code": "CUST01",
"account_label": "general",
"unstake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06757d",
"asset": "ETH",
"amount": "10.5",
"status": "failed",
"reason": "exit_queue_full"
}Implementation Best Practices
- Acknowledge immediately: Always return
200 OKto 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. For reward events, use
distribution_id. For stake and unstake events, usestake_id/unstake_idcombined withstatus. - 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.
- Pool capacity exceeded: Inform user and prompt retry.
- Terms revoked mid-flight: Block submission and prompt re-acceptance.
Error Response Format
All error responses follow this structure:
{
"error_code": "ERROR_IDENTIFIER",
"message": "Technical error description",
"details": {
"participant_code": "CUST01",
"stake_id": "c761fc96-5c44-40d4-8eb2-3fcd5d06754e",
"asset": "ETH",
"validation_checks": {
"jurisdiction_allowed": true,
"terms_accepted": false,
"platform_restrictions": "none",
"sufficient_balance": true
}
},
"user_message": "",
"request_id": "req_123456789",
"timestamp": "2026-05-21T16: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_notional": "35000.00",
"available": "8.2"
},
"user_message": "You do not have enough ETH to complete this stake"
}JURISDICTION_RESTRICTED
{
"error_code": "JURISDICTION_RESTRICTED",
"message": "Staking not available in user's jurisdiction",
"details": {
"participant_code": "CUST01",
"jurisdiction": "CA"
},
"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}
- Update terms acceptance: PATCH /participants/customers/{participant_code}
- Submit stake: POST /stakes
- Cancel pending stake: POST /stakes/{stake_id}/cancel
- Monitor stake status: GET /stakes/{stake_id}/status
- View portfolio: GET /stakes/{participant_code}
- Track rewards: GET /stakes/{participant_code}/rewards
- Submit unstake: POST /stakes/unstake
- Cancel pending unstake: POST /stakes/unstake/{unstake_id}/cancel
- Monitor unstake status: GET /stakes/unstake/{unstake_id}/status
Support and Resources
Please contact your dedicated Relationship Manager for:
- Frontend standards and implementation requirements
- Sample UX designs and the UX research report
- Setting your platform fee
- User education materials and user-facing FAQs
- Any additional integration support
Updated 17 days ago