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:

  1. HasaPay's path — derived from your organization's encrypted seed (unchanged from normal wallets).
  2. 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 holding cocustody:recover to 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, for recover, 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 fragment1 and fragment2 separately (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)