Co-Custody API
Co-custody lets your organization hold its own recoverable copy of a master wallet's private key, while HasaPay continues to manage and sign for the wallet. It is shared custody, not a hand-off: HasaPay keeps signing for sweeps, payouts, and management; your team additionally gets a key it can export and use anytime.
A co-custody master wallet's private key can be reconstructed two independent ways:
- HasaPay's path — derived from your organization's encrypted seed (unchanged from normal wallets).
- Your path — a passphrase you choose plus two fragments HasaPay returns to you. Combine all three to rebuild the key.
HasaPay never stores your passphrase or either fragment — only a non-secret salt. Lose any one of the three and the key is unrecoverable from your side (HasaPay's seed path is unaffected).
Base path: /api/v1/wallets/:walletId
Authentication
These endpoints accept either authentication mode. Co-custody must be enabled for your organization by HasaPay in both cases.
Dashboard (JWT)
A logged-in owner or admin with a Bearer token, plus a step-up 2FA code (mfa_code) in every request body. This is what the dashboard uses.
Authorization: Bearer <jwt>
API key (HMAC)
Programmatic access with your API key + request signature (X-API-Key, X-Signature, X-Timestamp — see the Authentication guide for signing). No mfa_code is used on this path. The key must be granted an explicit co-custody permission:
| Permission | Grants |
|---|---|
cocustody:manage |
Enable co-custody + generate fragments |
cocustody:recover |
Reconstruct the raw private key |
cocustody:* |
Both of the above |
A wildcard
*key is deliberately not sufficient for co-custody — because these endpoints mint key fragments and can export the raw master key, the permission must be granted on purpose. Strongly recommended: restrict any key holdingcocustody:recoverto your server IPs with the API-key IP allowlist. Unlike the dashboard path, an API key has no second factor — anyone with the key secret (and, forrecover, the fragments + passphrase) can use it.
Enable co-custody on a wallet
Upgrade an existing master wallet to co-custody in place. The wallet, address, and funds do not change — this only unlocks the key-fragment path. One-way (cannot be undone).
POST /api/v1/wallets/:walletId/enable-cocustody
API-key permission: cocustody:manage
Request body
| Field | Type | Required | Description |
|---|---|---|---|
mfa_code |
string | Dashboard only | Your authenticator (TOTP) or backup code. Required for JWT; not used for API-key (HMAC) requests. |
Example — dashboard (JWT)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/enable-cocustody \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "mfa_code": "123456" }'
Example — API key (HMAC)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/enable-cocustody \
-H "X-API-Key: $API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "Content-Type: application/json" \
-d '{}'
Response
{
"message": "Co-custody enabled. You can now generate key fragments for this wallet.",
"data": { "wallet_id": "uuid", "custody_type": "co_custody" }
}
New wallets can be created as co-custody directly from the dashboard's "Create wallet" flow (the co-custody option). Existing wallets use this endpoint.
Generate key fragments
Mint two passphrase-encrypted fragments of the wallet's private key. Shown once. Save both — HasaPay does not store them.
POST /api/v1/wallets/:walletId/fragments
API-key permission: cocustody:manage
Request body
| Field | Type | Required | Description |
|---|---|---|---|
passphrase |
string | Yes | A secret you choose (min 12 chars — use a long, unique, randomly-generated phrase). Needed later, with both fragments, to recover the key. Not stored by HasaPay. |
mfa_code |
string | Dashboard only | Step-up 2FA code. Required for JWT; not used for API-key (HMAC) requests. |
Example — dashboard (JWT)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/fragments \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "passphrase": "correct-horse-battery-staple-42", "mfa_code": "123456" }'
Example — API key (HMAC)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/fragments \
-H "X-API-Key: $API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "Content-Type: application/json" \
-d '{ "passphrase": "correct-horse-battery-staple-42" }'
Response
{
"message": "Save both fragments now. HasaPay does not store them. You need both fragments plus your passphrase to recover the key.",
"data": {
"fragment1": "base64…",
"fragment2": "base64…"
}
}
Store
fragment1andfragment2separately (for example, with two different people) so no single holder can reconstruct the key alone. Each fragment on its own reveals nothing.
Recover the private key
Reconstruct the master wallet's private key from your passphrase and both fragments.
POST /api/v1/wallets/:walletId/recover
API-key permission: cocustody:recover (stricter than cocustody:manage — this returns the raw private key)
Request body
| Field | Type | Required | Description |
|---|---|---|---|
passphrase |
string | Yes | The same passphrase used when the fragments were generated |
fragment1 |
string | Yes | First fragment |
fragment2 |
string | Yes | Second fragment |
mfa_code |
string | Dashboard only | Step-up 2FA code. Required for JWT; not used for API-key (HMAC) requests. |
Example — dashboard (JWT)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/recover \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"passphrase": "correct-horse-battery-staple-42",
"fragment1": "base64…",
"fragment2": "base64…",
"mfa_code": "123456"
}'
Example — API key (HMAC)
curl -X POST https://api.hasapay.com/api/v1/wallets/$WALLET_ID/recover \
-H "X-API-Key: $API_KEY" \
-H "X-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-H "Content-Type: application/json" \
-d '{
"passphrase": "correct-horse-battery-staple-42",
"fragment1": "base64…",
"fragment2": "base64…"
}'
Response
{
"message": "Private key reconstructed. Import it into your wallet, then discard this response.",
"data": { "private_key": "hex private key" }
}
A wrong passphrase or a missing/incorrect fragment fails cryptographically — no key is returned. Import the returned key into any standard wallet (MetaMask, TronLink, a Bitcoin wallet, etc.) for the wallet's chain.
How it works
- Splitting uses a 2-of-2 secret share: the key is divided into two fragments, both required to rebuild it. Each fragment alone is indistinguishable from random.
- Encryption stretches your passphrase with Argon2id and seals each fragment with AES-256-GCM. A leaked fragment is useless without the passphrase.
- HasaPay stores none of the three secret factors (passphrase, fragment1, fragment2) — only a non-secret salt. A breach of HasaPay's database cannot reconstruct your key.
What "we never store it" means
HasaPay never stores your passphrase, fragments, or recovered key. When you call /recover, those values pass through HasaPay's servers in memory for that one request to do the math — they are never written down and never logged. If you require true zero-knowledge (HasaPay never sees the values at all), combine the fragments offline with the HasaPay key tool instead of using /recover.
Shared custody, not sole custody
A recovered key gives you the ability to spend from the wallet — but HasaPay can still sign for it too (that is the point of co-custody). If you want sole custody, move the funds from the recovered wallet to a brand-new wallet you generated yourself, that HasaPay has never seen.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error |
Missing passphrase/fragments, or wallet is not co-custody |
| 401 | unauthorized |
No Bearer JWT and no API-key signature provided |
| 403 | forbidden |
(Dashboard) not an owner/admin · (API key) the key lacks the explicit cocustody:manage / cocustody:recover permission — a wildcard * is not sufficient |
| 403 | dashboard_feature_disabled |
Co-custody not enabled for your organization |
| 403 | mfa_required / mfa_invalid |
Missing or wrong step-up 2FA code (dashboard/JWT path only) |